diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index d235bb82c..f37b698d2 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -6,7 +6,15 @@ "version": "5.3.11", "commands": [ "reportgenerator" - ] + ], + "rollForward": false + }, + "dotnet-ef": { + "version": "9.0.8", + "commands": [ + "dotnet-ef" + ], + "rollForward": false } } } \ No newline at end of file diff --git a/.env.example b/.env.example index c207c6973..c1f8f97af 100644 --- a/.env.example +++ b/.env.example @@ -43,7 +43,7 @@ CONDUIT_API_BASE_URL=http://api:8080 CONDUIT_ADMIN_API_BASE_URL=http://admin:8080 # =========================================== -# External Service URLs (For WebUI/Clients) +# External Service URLs (For WebAdmin/Clients) # =========================================== CONDUIT_API_EXTERNAL_URL=http://localhost:5000 CONDUIT_ADMIN_API_EXTERNAL_URL=http://localhost:5002 @@ -51,23 +51,51 @@ CONDUIT_ADMIN_API_EXTERNAL_URL=http://localhost:5002 # =========================================== # Media Storage Configuration # =========================================== -# Storage provider: InMemory, S3 -# Option 1: Use configuration path format -# ConduitLLM__Storage__Provider=S3 -# Option 2: Use environment variable format (recommended for dev) +# ⚠️ CRITICAL FOR PRODUCTION: Without proper S3 configuration, media files are NEVER +# cleaned up when virtual keys are deleted, leading to unbounded storage costs! +# See docs/operations/deployment/media-cleanup-configuration.md for details. + +# Storage provider: InMemory (dev only), S3 (production) CONDUIT_MEDIA_STORAGE_TYPE=InMemory CONDUITLLM__MEDIA_BASE_URL=http://localhost:5000 -# S3 Configuration (if using S3 provider) -# For development with Cloudflare R2: +# IMPORTANT: Admin API needs the SAME storage configuration as Gateway API +# to enable automatic media cleanup when virtual keys are deleted. + +# =========================================== +# S3 Storage Configuration (Production) +# =========================================== +# Both Gateway API and Admin API must have these settings for media cleanup to work! + +# Option 1: Simplified format (recommended for Docker) +# CONDUIT_MEDIA_STORAGE_TYPE=S3 +# CONDUIT_S3_ENDPOINT=https://YOUR-ENDPOINT +# CONDUIT_S3_ACCESS_KEY_ID=your-access-key +# CONDUIT_S3_SECRET_ACCESS_KEY=your-secret-key +# CONDUIT_S3_BUCKET_NAME=conduit-media +# CONDUIT_S3_REGION=auto + +# Option 2: Hierarchical format (for direct .NET configuration) +# CONDUITLLM__STORAGE__PROVIDER=S3 +# CONDUITLLM__STORAGE__S3__SERVICEURL=https://YOUR-ENDPOINT +# CONDUITLLM__STORAGE__S3__ACCESSKEY=your-access-key +# CONDUITLLM__STORAGE__S3__SECRETKEY=your-secret-key +# CONDUITLLM__STORAGE__S3__BUCKETNAME=conduit-media +# CONDUITLLM__STORAGE__S3__REGION=auto + +# =========================================== +# S3 Provider Examples +# =========================================== + +# Cloudflare R2 (recommended - free egress, optimized multipart uploads) # CONDUIT_MEDIA_STORAGE_TYPE=S3 # CONDUIT_S3_ENDPOINT=https://YOUR-ACCOUNT-ID.r2.cloudflarestorage.com # CONDUIT_S3_ACCESS_KEY_ID=your-r2-access-key # CONDUIT_S3_SECRET_ACCESS_KEY=your-r2-secret-key -# CONDUIT_S3_BUCKET_NAME=conduit +# CONDUIT_S3_BUCKET_NAME=conduit-media # CONDUIT_S3_REGION=auto -# For production with AWS S3: +# AWS S3 # CONDUIT_MEDIA_STORAGE_TYPE=S3 # CONDUIT_S3_ENDPOINT=https://s3.amazonaws.com # CONDUIT_S3_ACCESS_KEY_ID=your-access-key @@ -75,13 +103,20 @@ CONDUITLLM__MEDIA_BASE_URL=http://localhost:5000 # CONDUIT_S3_BUCKET_NAME=conduit-media # CONDUIT_S3_REGION=us-east-1 -# For Cloudflare R2: -# CONDUIT_MEDIA_STORAGE_TYPE=S3 -# CONDUIT_S3_ENDPOINT=https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com -# CONDUIT_S3_ACCESS_KEY_ID=your-r2-access-key -# CONDUIT_S3_SECRET_ACCESS_KEY=your-r2-secret-key -# CONDUIT_S3_BUCKET_NAME=conduit-media -# CONDUIT_S3_REGION=auto +# =========================================== +# Media Lifecycle Management +# =========================================== +# ⚠️ CRITICAL: Enable these for automatic cleanup of orphaned media files + +# Auto-cleanup when virtual keys are deleted (recommended for production) +# CONDUITLLM__MEDIAMANAGEMENT__ENABLEAUTOCLEANUP=true + +# Periodic cleanup of orphaned media files (recommended for production) +# CONDUITLLM__MEDIAMANAGEMENT__ORPHANCLEANUPENABLED=true + +# Verification: Check Admin API logs on startup for: +# ✅ "[MediaLifecycleService] Deleted {Count} media files for virtual key {VirtualKeyId}" +# ❌ "[AdminVirtualKeyService] Media lifecycle service not available" = cleanup NOT working! # =========================================== # Performance Tracking @@ -145,13 +180,13 @@ CONDUIT_ADMIN_IP_BANNING_ENABLED=false CONDUIT_ADMIN_FAILED_AUTH_MAX_ATTEMPTS=5 CONDUIT_ADMIN_FAILED_AUTH_BAN_DURATION_MINUTES=30 -# Core API Security +# Gateway API Security CONDUIT_CORE_RATE_LIMITING_ENABLED=true CONDUIT_CORE_IP_FILTERING_ENABLED=false CONDUIT_CORE_SECURITY_HEADERS_ENABLED=true # =========================================== -# WebUI Configuration (Next.js) +# WebAdmin Configuration (Next.js) # =========================================== NODE_ENV=production PORT=3000 diff --git a/.github/SETUP_AUTO_VERSIONING.md b/.github/SETUP_AUTO_VERSIONING.md index be3279723..cffe45280 100644 --- a/.github/SETUP_AUTO_VERSIONING.md +++ b/.github/SETUP_AUTO_VERSIONING.md @@ -36,13 +36,13 @@ The workflow automatically runs when: - **Versions**: `1.1.0-dev.1`, `1.1.0-dev.2`, `1.1.0-dev.3`, etc. - **NPM Tag**: `@dev` - **Behavior**: Auto-increments prerelease number, no commits back to repo -- **Install**: `npm install @knn_labs/conduit-core-client@dev` +- **Install**: `npm install @knn_labs/conduit-gateway-client@dev` #### Master Branch (`origin/master`) - **Versions**: `1.1.0` → `1.1.1` → `1.1.2` (patch increments by default) - **NPM Tag**: `@latest` - **Behavior**: Commits version changes back to repo, publishes to NPM -- **Install**: `npm install @knn_labs/conduit-core-client@latest` +- **Install**: `npm install @knn_labs/conduit-gateway-client@latest` ### Smart Version Detection The workflow automatically detects version type from commit messages: @@ -103,16 +103,16 @@ You can also trigger versioning manually: ### NPM Package Status ```bash # Check latest dev versions -npm view @knn_labs/conduit-core-client versions --json | jq '.[] | select(test("dev"))' +npm view @knn_labs/conduit-gateway-client versions --json | jq '.[] | select(test("dev"))' # Check latest stable versions -npm view @knn_labs/conduit-core-client versions --json | jq '.[] | select(test("dev") | not)' +npm view @knn_labs/conduit-gateway-client versions --json | jq '.[] | select(test("dev") | not)' ``` ### Installation Commands The workflow provides installation commands in its output: -- Dev: `npm install @knn_labs/conduit-core-client@dev` -- Stable: `npm install @knn_labs/conduit-core-client@latest` +- Dev: `npm install @knn_labs/conduit-gateway-client@dev` +- Stable: `npm install @knn_labs/conduit-gateway-client@latest` ## 🛠️ Troubleshooting diff --git a/.github/SETUP_DOTNET_VERSIONING.md b/.github/SETUP_DOTNET_VERSIONING.md index 0b10e5267..4f61ec31e 100644 --- a/.github/SETUP_DOTNET_VERSIONING.md +++ b/.github/SETUP_DOTNET_VERSIONING.md @@ -44,7 +44,7 @@ The workflow automatically runs when: ✅ **ConduitLLM.Core** - Core interfaces and models ✅ **ConduitLLM.Providers** - LLM provider implementations -❌ **Applications not packaged**: Http, WebUI, Admin, Examples, Tests +❌ **Applications not packaged**: Http, WebAdmin, Admin, Examples, Tests #### Version Strategy diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 883a5f2b7..acec29409 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -13,7 +13,7 @@ This repository uses a simplified, industry-standard CI/CD pipeline. - Publishes NPM packages with `next` tag (only from `master`) **Artifacts produced from `master`:** -- Docker: `ghcr.io/knnlabs/conduit-{webui,http,admin}:latest` +- Docker: `ghcr.io/knnlabs/conduit-{webadmin,http,admin}:latest` - NPM: `@conduitllm/{admin,core}@next` ### 2. Release (`release.yml`) @@ -25,7 +25,7 @@ This repository uses a simplified, industry-standard CI/CD pipeline. - Publishes versioned NPM packages **Artifacts produced:** -- Docker: `ghcr.io/knnlabs/conduit-{webui,http,admin}:1.2.3` +- Docker: `ghcr.io/knnlabs/conduit-{webadmin,http,admin}:1.2.3` - NPM: `@conduitllm/{admin,core}@1.2.3` - GitHub Release with changelog diff --git a/.github/workflows/archive-2024-08/build-and-release.yml b/.github/workflows/archive-2024-08/build-and-release.yml index 988d615e5..886dcca62 100644 --- a/.github/workflows/archive-2024-08/build-and-release.yml +++ b/.github/workflows/archive-2024-08/build-and-release.yml @@ -277,9 +277,9 @@ jobs: strategy: matrix: include: - - service: webui - image: ghcr.io/knnlabs/conduit-webui - dockerfile: ./ConduitLLM.WebUI/Dockerfile + - service: webadmin + image: ghcr.io/knnlabs/conduit-webadmin + dockerfile: ./ConduitLLM.WebAdmin/Dockerfile - service: http image: ghcr.io/knnlabs/conduit-http dockerfile: ./ConduitLLM.Http/Dockerfile @@ -363,8 +363,8 @@ jobs: strategy: matrix: include: - - service: webui - image: ghcr.io/knnlabs/conduit-webui + - service: webadmin + image: ghcr.io/knnlabs/conduit-webadmin - service: http image: ghcr.io/knnlabs/conduit-http - service: admin @@ -450,6 +450,6 @@ jobs: echo "Docker images have been successfully built:" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Images are available at:" >> $GITHUB_STEP_SUMMARY - echo "- 🌐 `ghcr.io/knnlabs/conduit-webui` (linux/amd64)" >> $GITHUB_STEP_SUMMARY + echo "- 🌐 `ghcr.io/knnlabs/conduit-webadmin` (linux/amd64)" >> $GITHUB_STEP_SUMMARY echo "- 🔌 `ghcr.io/knnlabs/conduit-http` (linux/amd64)" >> $GITHUB_STEP_SUMMARY echo "- 🛠️ `ghcr.io/knnlabs/conduit-admin` (linux/amd64)" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/archive-2024-08/release-orchestration.yml b/.github/workflows/archive-2024-08/release-orchestration.yml index 60d04b4f9..cd182aacf 100644 --- a/.github/workflows/archive-2024-08/release-orchestration.yml +++ b/.github/workflows/archive-2024-08/release-orchestration.yml @@ -127,7 +127,7 @@ jobs: EOF if [[ "${{ steps.parse.outputs.release_docker }}" == "true" ]]; then - echo "- ✅ Docker Images (WebUI, HTTP API, Admin API)" >> release-notes.md + echo "- ✅ Docker Images (WebAdmin, HTTP API, Admin API)" >> release-notes.md fi echo "- ℹ️ NPM SDKs: Published separately via automated workflow" >> release-notes.md @@ -142,7 +142,7 @@ jobs: ### Docker \`\`\`bash - docker pull ghcr.io/knnlabs/conduit-webui:v$VERSION + docker pull ghcr.io/knnlabs/conduit-webadmin:v$VERSION docker pull ghcr.io/knnlabs/conduit-http:v$VERSION docker pull ghcr.io/knnlabs/conduit-admin:v$VERSION \`\`\` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e0f40a0f..dff2af76f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,8 +8,8 @@ on: env: REGISTRY: ghcr.io - DOTNET_VERSION: '9.0.x' - NODE_VERSION: '20' + DOTNET_VERSION: '10.0.x' + NODE_VERSION: '22' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -20,6 +20,29 @@ jobs: validate: name: Validate runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + ports: + - 5432:5432 + env: + POSTGRES_USER: conduit + POSTGRES_PASSWORD: conduitpass + POSTGRES_DB: conduitdb + options: >- + --health-cmd "pg_isready -U conduit -d conduitdb" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - uses: actions/checkout@v4 @@ -38,31 +61,32 @@ jobs: SDKs/Node/Admin/package-lock.json SDKs/Node/Core/package-lock.json SDKs/Node/Common/package-lock.json - ConduitLLM.WebUI/package-lock.json + WebAdmin/package-lock.json - - name: Validate PostgreSQL migrations - run: ./scripts/migrations/validate-postgresql-syntax.sh - - name: .NET Build & Test run: | dotnet restore dotnet build --no-restore # Run tests but exclude integration tests and timing-sensitive tests dotnet test --no-build --verbosity normal --filter "FullyQualifiedName!~IntegrationTests&Category!=TimingSensitive" + env: + TEST_REDIS_CONNECTION: localhost:6379 + DATABASE_URL: postgresql://conduit:conduitpass@localhost:5432/conduitdb - name: NPM Build run: | cd SDKs/Node npm ci npm run build - npm test -- --silent + npm run test:ci - - name: WebUI Type Check + - name: WebAdmin Lint & Type Check run: | - cd ConduitLLM.WebUI + cd WebAdmin npm ci # Clean any stale .next directory before type checking rm -rf .next + npm run lint npm run type-check # Build Docker images (push only from master) @@ -76,15 +100,15 @@ jobs: strategy: matrix: include: - - service: webui + - service: webadmin context: . - dockerfile: ConduitLLM.WebUI/Dockerfile + dockerfile: WebAdmin/Dockerfile - service: http context: . - dockerfile: ConduitLLM.Http/Dockerfile + dockerfile: Services/ConduitLLM.Gateway/Dockerfile - service: admin context: . - dockerfile: ConduitLLM.Admin/Dockerfile + dockerfile: Services/ConduitLLM.Admin/Dockerfile steps: - uses: actions/checkout@v4 @@ -150,10 +174,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 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7ae807830..8ffff2504 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -28,7 +28,7 @@ concurrency: cancel-in-progress: false env: - DOTNET_VERSION: '9.0.x' + DOTNET_VERSION: '10.0.x' DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: true @@ -124,14 +124,14 @@ jobs: if: matrix.language == 'javascript' uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'npm' cache-dependency-path: | SDKs/Node/package-lock.json SDKs/Node/Admin/package-lock.json SDKs/Node/Core/package-lock.json SDKs/Node/Common/package-lock.json - ConduitLLM.WebUI/package-lock.json + WebAdmin/package-lock.json - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/deprecated/docker-release.yml b/.github/workflows/deprecated/docker-release.yml index e24cd1c6e..20c9747a3 100644 --- a/.github/workflows/deprecated/docker-release.yml +++ b/.github/workflows/deprecated/docker-release.yml @@ -40,12 +40,12 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # Add metadata for the WebUI image - - name: Extract metadata for WebUI - id: meta-webui + # Add metadata for the WebAdmin image + - name: Extract metadata for WebAdmin + id: meta-webadmin uses: docker/metadata-action@v5 with: - images: ghcr.io/knnlabs/conduit-webui + images: ghcr.io/knnlabs/conduit-webadmin tags: | type=ref,event=branch type=ref,event=pr @@ -53,15 +53,15 @@ jobs: type=sha,format=long type=raw,value=latest,enable=${{ github.ref_name == 'master' }} - # Build and push WebUI Docker image - - name: Build and push WebUI Docker image + # Build and push WebAdmin Docker image + - name: Build and push WebAdmin Docker image uses: docker/build-push-action@v5 with: context: . - file: ./ConduitLLM.WebUI/Dockerfile + file: ./ConduitLLM.WebAdmin/Dockerfile push: true - tags: ${{ steps.meta-webui.outputs.tags }} - labels: ${{ steps.meta-webui.outputs.labels }} + tags: ${{ steps.meta-webadmin.outputs.tags }} + labels: ${{ steps.meta-webadmin.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max # Add build args for better diagnostics diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce2ad5d3e..ea8e61bee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,26 +46,35 @@ jobs: packages: write strategy: matrix: - service: [webui, http, admin] - + include: + - service: webadmin + context: . + dockerfile: WebAdmin/Dockerfile + - service: http + context: . + dockerfile: Services/ConduitLLM.Gateway/Dockerfile + - service: admin + context: . + dockerfile: Services/ConduitLLM.Admin/Dockerfile + steps: - uses: actions/checkout@v4 - + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - + - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - + - name: Build and push uses: docker/build-push-action@v5 with: - context: . - file: ConduitLLM.${{ matrix.service == 'webui' && 'WebUI' || (matrix.service == 'http' && 'Http' || 'Admin') }}/Dockerfile + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} platforms: linux/amd64 push: true tags: | @@ -85,7 +94,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' registry-url: 'https://registry.npmjs.org' cache: npm cache-dependency-path: | diff --git a/.gitignore b/.gitignore index dd3723137..34b63db00 100644 --- a/.gitignore +++ b/.gitignore @@ -435,4 +435,16 @@ codeql-db/ codeql-results/ *.sarif qlconfig.yml -.codeql-build.sh \ No newline at end of file +.codeql-build.sh + +# CSS linting cache +.stylelintcache + +# SQL seed and model files +db-seed.sql +functions-seed.sql +replicate-images-models.sql +replicate-text-models.sql +replicate-video-models.sql +scripts/db/replicate/replicate-text-models.sql +scripts/db/replicate/replicate-video-models.sql diff --git a/.gitleaks.toml b/.gitleaks.toml index f5ce048ee..b0308029c 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -15,9 +15,9 @@ regex = '''(?i)(conduit[_-]?master[_-]?key\s*[:=]\s*)["']?([a-zA-Z0-9+/]{32,})[" secretGroup = 2 [[rules]] -id = "conduit-webui-auth-key" -description = "Conduit WebUI Auth Key" -regex = '''(?i)(conduit[_-]?webui[_-]?auth[_-]?key\s*[:=]\s*)["']?([a-zA-Z0-9+/]{32,})["']?''' +id = "conduit-webadmin-auth-key" +description = "Conduit WebAdmin Auth Key" +regex = '''(?i)(conduit[_-]?webadmin[_-]?auth[_-]?key\s*[:=]\s*)["']?([a-zA-Z0-9+/]{32,})["']?''' secretGroup = 2 [[rules]] @@ -71,7 +71,7 @@ regexes = [ # Conduit-specific sample values (safe to commit) commits = [ '''alpha''', # Sample CONDUIT_MASTER_KEY in docker-compose.yml - '''conduit123''', # Sample CONDUIT_WEBUI_AUTH_KEY in docker-compose.yml + '''conduit123''', # Sample CONDUIT_WEBADMIN_AUTH_KEY in docker-compose.yml ] # Entropy settings diff --git a/.husky/pre-push b/.husky/pre-push index 447c19ae4..d1dbf8cd5 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,6 +1,3 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - # Pre-push hook to prevent pushing code with lint errors echo "🔍 Running pre-push lint checks..." @@ -12,7 +9,7 @@ if [ "$CI" = "true" ]; then fi # Run the STRICT validation script (same as CI/CD) -./scripts/validate-eslint-strict.sh +./scripts/test/validate-eslint-strict.sh if [ $? -ne 0 ]; then echo "" @@ -21,9 +18,9 @@ if [ $? -ne 0 ]; then echo "⚠️ You cannot push code with lint errors. This causes CI/CD failures." echo "" echo "To fix:" - echo "1. Run './scripts/fix-lint-errors.sh' to automatically fix what's possible" + echo "1. Run './scripts/dev/fix-webadmin-errors.sh' to automatically fix what's possible" echo "2. Manually fix any remaining errors" - echo "3. Test locally with './scripts/validate-eslint.sh'" + echo "3. Test locally with './scripts/test/validate-eslint.sh'" echo "" echo "To bypass (NOT RECOMMENDED):" echo "git push --no-verify" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..a07a315ec --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,934 @@ +# Agents Guide + +*Last Updated: 2025-11-25* + +Comprehensive guide for building and deploying AI agents using ConduitLLM's agentic workflows and function calling capabilities. + +## Table of Contents +- [Overview](#overview) +- [Agent Architecture](#agent-architecture) +- [Configuration](#configuration) +- [Building Agents](#building-agents) +- [Deployment & Operations](#deployment--operations) +- [Examples & Templates](#examples--templates) +- [Advanced Features](#advanced-features) +- [Troubleshooting](#troubleshooting) +- [Related Documentation](#related-documentation) + +--- + +## Overview + +### What are Agents in ConduitLLM? + +In ConduitLLM, **agents** are AI systems that can autonomously execute functions and tools to accomplish complex tasks. Unlike simple chat completions, agents can: + +- **Make decisions** about when to call functions +- **Execute multiple functions** in a single workflow +- **Handle dependencies** between function calls +- **Iterate** on results until a task is complete +- **Provide real-time feedback** during execution + +### Agentic Mode vs Manual Function Calling + +ConduitLLM supports two approaches to function calling: + +#### Manual Function Calling +- Your application receives `tool_calls` from the LLM +- You decide which functions to execute and how +- You control the flow and iteration logic +- Full control but more complex implementation + +#### Agentic Mode (Recommended for Agents) +- Conduit automatically executes requested functions +- Handles dependency detection and parallel/sequential execution +- Manages iteration limits and cost tracking +- Provides real-time streaming events for progress +- Simplifies agent development significantly + +### Use Cases + +Agents are ideal for: +- **Data analysis workflows** (query databases, process results, generate reports) +- **API integration** (call external services, transform data, take actions) +- **Content creation pipelines** (research, draft, edit, publish) +- **Customer service automation** (lookup records, take actions, respond) +- **Development assistants** (read code, run tests, suggest fixes) + +--- + +## Agent Architecture + +### Agentic Workflow Flow + +```mermaid +graph TD + A[User Request] --> B[LLM Analysis] + B --> C{Functions Needed?} + C -->|No| D[Direct Response] + C -->|Yes| E[Function Calls Generated] + E --> F[Dependency Detection] + F --> G{Dependencies?} + G -->|No| H[Parallel Execution] + G -->|Yes| I[Sequential Execution] + H --> J[Function Results] + I --> J + J --> K[Results Fed Back to LLM] + K --> L{More Functions Needed?} + L -->|Yes| B + L -->|No| M[Final Response] + M --> N[Agentic Metrics] +``` + +### Core Components + +#### 1. AgenticOrchestrationService +The brain of agent execution: +- **Function Discovery**: Maps function names to configurations +- **Dependency Analysis**: Detects when functions depend on each other +- **Execution Strategy**: Chooses parallel vs sequential execution +- **Cost Tracking**: Monitors costs across all function calls +- **Error Handling**: Manages failures and retries + +#### 2. Function Configuration +Functions are configured via the Admin API: +```json +{ + "name": "get_customer_data", + "description": "Retrieves customer information from the database", + "parameters": { + "type": "object", + "properties": { + "customer_id": { + "type": "string", + "description": "The customer ID to lookup" + } + }, + "required": ["customer_id"] + }, + "endpoint": "https://api.example.com/customers/{customer_id}", + "method": "GET", + "headers": { + "Authorization": "Bearer {API_KEY}" + } +} +``` + +#### 3. Virtual Keys +Agents use virtual keys for: +- **Authentication**: Secure access to Conduit APIs +- **Cost Tracking**: All function calls billed to the virtual key +- **Rate Limiting**: Per-agent rate limits and quotas +- **Access Control**: Restrict which functions agents can call + +### Execution Modes + +#### Parallel Execution +- Used when functions have no dependencies +- All functions execute simultaneously +- Faster completion for independent tasks +- Example: Get user profile AND get order history + +#### Sequential Execution +- Used when functions depend on each other +- Functions execute one after another +- Ensures data availability for dependent calls +- Example: Get customer ID → Get customer details → Update CRM + +--- + +## Configuration + +### Global Agentic Settings + +Configure global defaults via the Admin API or database: + +```sql +-- Maximum iterations per request +INSERT INTO "GlobalSettings" ("Key", "Value", "Description") +VALUES ('Agentic.MaxIterations', '5', 'Maximum agentic loop iterations (1-100)'); + +-- Minimum iterations (rarely used) +INSERT INTO "GlobalSettings" ("Key", "Value", "Description") +VALUES ('Agentic.MinIterations', '1', 'Minimum agentic iterations (1-100)'); + +-- Default agentic mode state +INSERT INTO "GlobalSettings" ("Key", "Value", "Description") +VALUES ('Agentic.DefaultEnabled', 'true', 'Enable agentic mode by default'); +``` + +### Virtual Key Configuration + +Create virtual keys specifically for agents: + +```bash +# Create virtual key for agent +curl -X POST http://localhost:5002/api/virtual-keys \ + -H "Authorization: Bearer YOUR_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "customer-service-agent", + "budget_limit": 100.00, + "allowed_models": ["gpt-4", "claude-3-sonnet"], + "function_configuration_ids": [1, 2, 3], + "max_requests_per_minute": 60 + }' +``` + +### Function Configuration + +#### HTTP Functions +```json +{ + "name": "send_email", + "description": "Sends an email to a recipient", + "parameters": { + "type": "object", + "properties": { + "to": {"type": "string", "description": "Recipient email"}, + "subject": {"type": "string", "description": "Email subject"}, + "body": {"type": "string", "description": "Email body"} + }, + "required": ["to", "subject", "body"] + }, + "endpoint": "https://api.emailservice.com/send", + "method": "POST", + "headers": { + "Authorization": "Bearer {EMAIL_API_KEY}", + "Content-Type": "application/json" + } +} +``` + +#### Database Functions +```json +{ + "name": "query_database", + "description": "Executes a SQL query on the customer database", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "SQL query to execute"} + }, + "required": ["query"] + }, + "endpoint": "internal://database/query", + "method": "POST" +} +``` + +### Provider Selection for Agents + +Choose providers based on agent requirements: + +| Provider | Best For | Function Calling | Speed | Cost | +|----------|----------|------------------|-------|------| +| OpenAI GPT-4 | Complex reasoning | Excellent | Medium | High | +| Anthropic Claude | Analysis tasks | Excellent | Medium | High | +| Groq Llama | Fast responses | Good | Very Fast | Low | +| Cerebras | Ultra-fast inference | Good | Ultra Fast | Medium | + +--- + +## Building Agents + +### Function Design Patterns + +#### 1. Atomic Functions +Keep functions focused and single-purpose: +```json +{ + "name": "get_customer_by_id", + "description": "Retrieves a single customer by their ID" +} +``` + +#### 2. Composite Functions +For complex operations, use multiple simple functions: +```json +{ + "name": "get_customer_profile", + "description": "Gets complete customer profile including orders and preferences" +} +// Agent will call: +// - get_customer_by_id +// - get_customer_orders +// - get_customer_preferences +``` + +#### 3. Validation Functions +Include validation in your function chain: +```json +{ + "name": "validate_customer_data", + "description": "Validates customer data before processing" +} +``` + +### Error Handling Patterns + +#### 1. Graceful Degradation +```json +{ + "name": "get_customer_data", + "description": "Retrieves customer data with fallback to cache" +} +// Agent logic: +// Try primary database → If fails, try cache → Return partial data +``` + +#### 2. Retry Logic +Configure retry policies in function definitions: +```json +{ + "retry_policy": { + "max_attempts": 3, + "backoff_strategy": "exponential", + "delay_ms": 1000 + } +} +``` + +#### 3. Error Recovery +Provide functions that can recover from errors: +```json +{ + "name": "reset_customer_session", + "description": "Resets customer session after errors" +} +``` + +### Streaming with Real-time Feedback + +Enable enhanced streaming for agent progress: + +```typescript +import { ConduitCoreClient } from '@knn_labs/conduit-gateway-client'; + +const client = new ConduitCoreClient({ + apiKey: 'condt_your_agent_key', + baseURL: 'http://localhost:5000' +}); + +const stream = await client.chat.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: 'Analyze customer churn risk' }], + stream: true, + agentic_mode: true, + function_configuration_ids: ['customer-analysis-functions'], + onToolExecuting: (event) => { + console.log(`Executing ${event.function_name}...`); + // Update UI with progress + }, + onToolResult: (event) => { + console.log(`Function ${event.tool_call_id} completed`); + // Update UI with results + } +}); + +for await (const event of stream) { + if (event.type === 'content') { + // Display LLM reasoning + console.log(event.choices[0]?.delta?.content); + } else if (event.type === 'final_metrics') { + // Show final agent metrics + console.log(`Total cost: $${event.total_cost}`); + console.log(`Functions called: ${event.total_function_calls}`); + } +} +``` + +### Best Practices + +#### Function Naming +- Use descriptive, action-oriented names +- Be consistent with naming conventions +- Include the data source in the name: `get_user_from_db`, `query_crm_api` + +#### Parameter Design +- Use clear parameter names and descriptions +- Provide reasonable defaults where possible +- Validate parameters at the function level +- Use JSON Schema for type safety + +#### Response Format +- Return structured, consistent data +- Include metadata (timestamps, sources, confidence) +- Handle errors gracefully with informative messages +- Consider response size for performance + +#### Security +- Never expose sensitive data in function descriptions +- Use parameter validation to prevent injection attacks +- Implement proper authentication for external APIs +- Log function calls for audit trails + +--- + +## Deployment & Operations + +### Scaling Considerations + +#### Horizontal Scaling +- Deploy multiple Conduit instances behind a load balancer +- Use Redis for distributed caching and session management +- Configure RabbitMQ for reliable message queuing +- Monitor queue depths and processing times + +#### Function Execution Scaling +```yaml +# docker-compose.yml for high-throughput agents +services: + conduit-http: + image: ghcr.io/knnlabs/conduit-http:latest + environment: + CONDUIT_FUNCTION_EXECUTION_CONCURRENCY: 50 + CONDUIT_FUNCTION_TIMEOUT_SECONDS: 30 + CONDUIT_ENABLE_PARALLEL_EXECUTION: "true" + deploy: + replicas: 3 +``` + +#### Database Scaling +- Use connection pooling for function database access +- Consider read replicas for data-intensive functions +- Monitor database query performance +- Optimize function queries with proper indexing + +### Monitoring Agent Performance + +#### Key Metrics +- **Function Call Latency**: Average time per function execution +- **Agentic Iteration Count**: How many loops per request +- **Cost per Request**: Track agent costs over time +- **Error Rates**: Function failure and retry rates +- **Concurrent Agents**: Number of simultaneous agent workflows + +#### Monitoring Setup +```yaml +# Prometheus configuration for agent metrics +scrape_configs: + - job_name: 'conduit-agents' + static_configs: + - targets: ['conduit-http:8080'] + metrics_path: '/metrics' + scrape_interval: 15s +``` + +#### Grafana Dashboard +Create dashboards to visualize: +- Agent request volume and success rates +- Function execution performance +- Cost tracking by virtual key +- Error analysis and alerting + +### Cost Management + +#### Budget Controls +```bash +# Set budget limits per agent +curl -X PUT http://localhost:5002/api/virtual-keys/{key_id}/budget \ + -H "Authorization: Bearer YOUR_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "daily_limit": 50.00, + "monthly_limit": 1000.00, + "alert_threshold": 0.80 + }' +``` + +#### Cost Optimization Strategies +1. **Model Selection**: Use faster, cheaper models for simple tasks +2. **Function Caching**: Cache expensive function results +3. **Batch Operations**: Combine multiple operations in single functions +4. **Early Termination**: Set reasonable iteration limits +5. **Provider Failover**: Switch to cheaper providers when possible + +### Security Considerations + +#### Function Security +- Validate all function inputs +- Sanitize outputs before returning to LLM +- Use principle of least privilege for API access +- Implement rate limiting per function + +#### Agent Isolation +- Use separate virtual keys for different agent types +- Network segmentation for external function calls +- Audit logging for all function executions +- Regular security reviews of function configurations + +#### Data Protection +- Encrypt sensitive data in function parameters +- Use secure connections for external APIs +- Implement data retention policies +- Consider privacy implications of agent data access + +--- + +## Examples & Templates + +### Simple Customer Service Agent + +#### Configuration +```json +{ + "name": "customer-service-agent", + "functions": [ + "get_customer_by_id", + "get_customer_orders", + "update_customer_address", + "create_support_ticket" + ], + "model": "gpt-4", + "max_iterations": 3, + "budget_limit": 10.00 +} +``` + +#### Implementation +```typescript +const customerServiceAgent = { + async handleRequest(customerQuery: string) { + const response = await conduitClient.chat.create({ + model: 'gpt-4', + messages: [ + { + role: 'system', + content: `You are a helpful customer service agent. + Use the available functions to assist customers. + Always verify customer information before making changes.` + }, + { + role: 'user', + content: customerQuery + } + ], + agentic_mode: true, + function_configuration_ids: ['customer-service-functions'], + stream: true, + max_iterations: 3 + }); + + return response; + } +}; +``` + +### Data Analysis Agent + +#### Function Definitions +```json +{ + "functions": [ + { + "name": "query_database", + "description": "Execute SQL queries on the analytics database" + }, + { + "name": "generate_chart", + "description": "Create charts from data" + }, + { + "name": "export_to_csv", + "description": "Export data to CSV format" + }, + { + "name": "send_report", + "description": "Email analysis report to stakeholders" + } + ] +} +``` + +#### Usage Example +```python +# Python example using OpenAI SDK with Conduit +from openai import OpenAI + +client = OpenAI( + api_key="condt_data_analysis_key", + base_url="https://conduit.yourcompany.com/v1" +) + +def analyze_sales_data(request: str): + response = client.chat.completions.create( + model="gpt-4", + messages=[ + {"role": "system", "content": "You are a data analyst. Use functions to query data, create visualizations, and generate reports."}, + {"role": "user", "content": request} + ], + tools=function_definitions, + agentic_mode=True, + max_iterations=5, + stream=True + ) + + return response +``` + +### Multi-Provider Agent Strategy + +#### Provider Configuration +```json +{ + "agent_strategy": { + "simple_queries": { + "provider": "groq", + "model": "llama-3.1-8b-instant", + "max_cost": 0.01 + }, + "complex_analysis": { + "provider": "openai", + "model": "gpt-4", + "max_cost": 0.50 + }, + "code_generation": { + "provider": "anthropic", + "model": "claude-3-sonnet", + "max_cost": 0.30 + } + } +} +``` + +#### Implementation +```typescript +class SmartAgent { + async processRequest(request: string, complexity: 'simple' | 'complex' | 'code') { + const config = this.agent_strategy[complexity]; + + return await conduitClient.chat.create({ + model: config.model, + provider: config.provider, + messages: [{ role: 'user', content: request }], + agentic_mode: true, + cost_limit: config.max_cost + }); + } +} +``` + +### Agent Template Library + +#### Research Agent +```typescript +const researchAgentTemplate = { + system_prompt: "You are a research assistant. Find, analyze, and synthesize information from multiple sources.", + functions: ["search_web", "fetch_document", "extract_text", "summarize_content", "cite_sources"], + max_iterations: 5, + requirements: ["web_access", "document_processing"] +}; +``` + +#### E-commerce Agent +```typescript +const ecommerceAgentTemplate = { + system_prompt: "You are an e-commerce assistant. Help customers with shopping, orders, and support.", + functions: ["search_products", "get_product_details", "check_inventory", "process_order", "track_shipment"], + max_iterations: 4, + requirements: ["product_catalog", "order_management", "inventory_system"] +}; +``` + +--- + +## Advanced Features + +### Custom Orchestration + +#### Override Default Behavior +```typescript +const customAgent = await conduitClient.chat.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: 'Complex task' }], + agentic_mode: true, + orchestration_config: { + execution_strategy: 'sequential', // Force sequential + dependency_detection: false, // Disable auto-detection + custom_timeout: 60, // 60 second timeout + retry_policy: { + max_retries: 2, + backoff: 'linear' + } + } +}); +``` + +#### Custom Function Execution +```csharp +// Implement custom function execution logic +public class CustomOrchestrationService : IAgenticOrchestrationService +{ + public async Task ExecuteToolCallsAsync( + List toolCalls, + int virtualKeyId, + // ... parameters + ) + { + // Custom execution logic + // - Add caching + // - Implement custom retry logic + // - Add monitoring hooks + // - Custom error handling + } +} +``` + +### Agent Memory Management + +#### Context Persistence +```typescript +const memoryAgent = { + async conversationWithMemory(userId: string, message: string) { + // Load conversation history + const history = await this.loadConversationHistory(userId); + + const response = await conduitClient.chat.create({ + model: 'gpt-4', + messages: [ + ...history, + { role: 'user', content: message } + ], + agentic_mode: true, + memory_config: { + store_context: true, + max_context_length: 10000, + summarization_threshold: 8000 + } + }); + + // Save updated conversation + await this.saveConversationHistory(userId, response); + + return response; + } +}; +``` + +#### State Management +```json +{ + "agent_state": { + "conversation_id": "uuid", + "user_preferences": {}, + "previous_actions": [], + "context_summary": "Brief summary of long conversation", + "next_steps": ["pending_action_1", "pending_action_2"] + } +} +``` + +### Performance Optimization + +#### Function Caching +```json +{ + "cache_config": { + "enabled": true, + "ttl_seconds": 300, + "cache_key_strategy": "parameters_hash", + "max_cache_size": "100MB" + } +} +``` + +#### Batch Processing +```typescript +const batchAgent = { + async processBatch(requests: string[]) { + const results = await Promise.all( + requests.map(request => + conduitClient.chat.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: request }], + agentic_mode: true, + batch_mode: true + }) + ) + ); + + return results; + } +}; +``` + +#### Connection Pooling +```yaml +# Optimize for high-throughput agent scenarios +environment: + CONDUIT_HTTP_CLIENT_POOL_SIZE: 100 + CONDUIT_FUNCTION_EXECUTION_POOL_SIZE: 50 + CONDUIT_ENABLE_KEEP_ALIVE: "true" + CONDUIT_CONNECTION_TIMEOUT_SECONDS: 30 +``` + +--- + +## Troubleshooting + +### Common Issues + +#### Agent Gets Stuck in Loops +**Symptoms**: Agent exceeds iteration limits, repetitive function calls + +**Solutions**: +1. Check function descriptions for clarity +2. Add validation functions to prevent redundant calls +3. Reduce `max_iterations` setting +4. Review LLM responses for confusion + +```typescript +// Add iteration monitoring +const response = await conduitClient.chat.create({ + agentic_mode: true, + max_iterations: 3, + onIteration: (iteration, context) => { + if (iteration > 2) { + console.warn('Agent approaching iteration limit'); + // Optionally intervene or modify behavior + } + } +}); +``` + +#### Function Execution Timeouts +**Symptoms**: Functions fail with timeout errors, incomplete agent workflows + +**Solutions**: +1. Increase function timeout settings +2. Optimize slow function implementations +3. Add progress callbacks for long-running functions +4. Implement function-specific retry policies + +```json +{ + "function_config": { + "timeout_seconds": 60, + "retry_policy": { + "max_attempts": 3, + "backoff_strategy": "exponential" + } + } +} +``` + +#### High Costs +**Symptoms**: Unexpectedly high API costs, rapid budget depletion + +**Solutions**: +1. Monitor function call patterns +2. Implement cost limits per request +3. Use cheaper models for simple tasks +4. Add function result caching + +```typescript +const costControlledAgent = await conduitClient.chat.create({ + agentic_mode: true, + cost_limit: 0.50, // Maximum cost per request + onCostUpdate: (currentCost) => { + if (currentCost > 0.40) { + console.warn('Approaching cost limit'); + } + } +}); +``` + +#### Function Dependency Issues +**Symptoms**: Functions fail due to missing data, incorrect execution order + +**Solutions**: +1. Review function parameter requirements +2. Ensure data flow between functions +3. Use explicit function sequencing when needed +4. Add data validation functions + +```typescript +// Force sequential execution for dependent functions +const response = await conduitClient.chat.create({ + agentic_mode: true, + execution_strategy: 'sequential', // Override parallel execution + functions: ['get_user_id', 'get_user_data', 'process_user_data'] +}); +``` + +### Debugging Tools + +#### Enable Detailed Logging +```yaml +# Development environment +environment: + CONDUIT_LOG_LEVEL: Debug + CONDUIT_ENABLE_FUNCTION_LOGGING: "true" + CONDUIT_ENABLE_AGENT_TRACING: "true" +``` + +#### Agent Execution Traces +```typescript +const response = await conduitClient.chat.create({ + agentic_mode: true, + enable_tracing: true, + trace_config: { + include_function_inputs: true, + include_function_outputs: true, + include_llm_reasoning: true + } +}); + +// Access execution trace +console.log(response.execution_trace); +``` + +#### Function Testing +```bash +# Test individual functions +curl -X POST http://localhost:5002/api/functions/test \ + -H "Authorization: Bearer YOUR_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "function_id": 123, + "test_parameters": { + "customer_id": "test-123" + } + }' +``` + +### Performance Tuning + +#### Monitor Key Metrics +```bash +# Check agent performance +curl http://localhost:5000/metrics | grep agent_ + +# Key metrics to watch: +# - agent_requests_total +# - agent_iterations_total +# - agent_function_calls_total +# - agent_cost_total +# - agent_duration_seconds +``` + +#### Optimization Checklist +- [ ] Function responses are optimized for size +- [ ] Parallel execution used where possible +- [ ] Appropriate models selected for tasks +- [ ] Caching enabled for expensive operations +- [ ] Connection pooling configured +- [ ] Database queries optimized +- [ ] Error handling doesn't cause excessive retries + +--- + +## Related Documentation + +- [Function Calling Guide](docs/api-guides/features/function-calling.md) - Technical details for function calling +- [Streaming with Tools](docs/api-guides/streaming-with-tools.md) - Real-time agent progress tracking +- [Gateway API Reference](docs/api-reference/core-api-reference.md) - Complete API documentation +- [Virtual Keys](docs/Virtual-Keys.md) - Authentication and cost tracking +- [Provider Integration](docs/Provider-Integration.md) - Multi-provider configuration +- [Architecture Overview](docs/architecture/README.md) - System design and patterns +- [Operations Documentation](docs/operations/README.md) - Deployment and monitoring + +--- + +*This guide is a living document. For the latest updates and examples, check the ConduitLLM documentation and GitHub repository.* diff --git a/CLAUDE.md b/CLAUDE.md index 82adb095b..0fafd7518 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,154 +2,169 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -**Last Reviewed**: 2025-08-07 (Corrected to match actual codebase implementation) +## Table of Contents +1. [⚠️ CRITICAL: Safety First](#️-critical-safety-first) +2. [Quick Start Guide](#quick-start-guide) +3. [Development Environment](#development-environment) +4. [Build & Verification](#build--verification) +5. [Code Quality Standards](#code-quality-standards) +6. [Architecture Essentials](#architecture-essentials) +7. [Documentation Index](#documentation-index) +8. [Repository & Collaboration](#repository--collaboration) -## Collaboration Guidelines -- **Challenge and question**: Don't immediately agree or proceed with requests that seem suboptimal, unclear, or potentially problematic -- **Push back constructively**: If a proposed approach has issues, suggest better alternatives with clear reasoning -- **Think critically**: Consider edge cases, performance implications, maintainability, and best practices before implementing -- **Seek clarification**: Ask follow-up questions when requirements are ambiguous or could be interpreted multiple ways -- **Propose improvements**: Suggest better patterns, more robust solutions, or cleaner implementations when appropriate -- **Be a thoughtful collaborator**: Act as a good teammate who helps improve the overall quality and direction of the project - -## Repository Information -- **GitHub Repository**: knnlabs/Conduit -- **Issues URL**: https://github.com/knnlabs/Conduit/issues -- **Pull Requests URL**: https://github.com/knnlabs/Conduit/pulls +--- -## CRITICAL SECURITY: Authentication -**WebUI Authentication**: The WebUI now uses Clerk for authentication. Human administrators authenticate through Clerk, not through password-based authentication. +# ⚠️ CRITICAL: Safety First -**Backend Authentication Key**: -- **CONDUIT_API_TO_API_BACKEND_AUTH_KEY**: - - Used by WebUI backend to authenticate with the Core API and Admin API - - This is for server-to-server communication between backend services - - NOT for end-users or client applications - - Configured on the WebUI service to talk to other backend services +## Commands That WILL Break Development -## Development Workflow - CRITICAL -**⚠️ CANONICAL DEVELOPMENT STARTUP: Always use `./scripts/start-dev.sh` for development** +### ❌ FORBIDDEN WEBADMIN COMMANDS +**These commands break the development container and force a 5+ minute restart:** +- `npm run build` (anywhere in WebAdmin directory) +- `cd WebAdmin && npm run build` +- `./scripts/dev/dev-workflow.sh build-webadmin` (production testing only) -### Starting Development Environment +**Why?** The development container uses an isolated `.next` directory. Running npm build on the host corrupts the container's build state. -#### Available Flags: -```bash -# Standard startup (builds containers if needed) -./scripts/start-dev.sh +### ✅ SAFE WEBADMIN VERIFICATION +Use these instead: +- `npm run lint` - Check ESLint errors +- `npm run type-check` - Verify TypeScript types +- Hot reloading automatically validates code changes -# Rebuild WebUI container (fixes Next.js issues) -./scripts/start-dev.sh --webui +### ❌ FORBIDDEN DEVELOPMENT COMMANDS +- `docker compose up` for development (always use `./scripts/dev/start-dev.sh`) -# Complete reset (removes all volumes and containers) -./scripts/start-dev.sh --clean +**If you run forbidden commands, you will:** +1. Break the development environment +2. Force a restart with `--clean` (5+ minutes) +3. Waste time and ignore explicit instructions -# Force rebuild containers with no cache -./scripts/start-dev.sh --build -``` +--- -#### What Each Flag Actually Does: -- **--webui**: Restarts WebUI container, cleans .next build artifacts -- **--clean**: Removes all containers, volumes, node_modules, and build artifacts for fresh start -- **--build**: Rebuilds containers with `--no-cache` flag -- **--help**: Shows usage information +# Quick Start Guide -#### Key Features: -- ✅ Node modules exist on HOST - direct npm command access -- ✅ WebUI directory mounted for hot reloading -- ✅ User ID mapping prevents permission issues (uses your UID/GID) -- ✅ Development containers use node:22-alpine directly +## Starting Development Services -### Build Commands -- Build entire solution: `dotnet build` -- Run all tests: `dotnet test` -- Run specific test: `dotnet test --filter "FullyQualifiedName=ConduitLLM.Tests.TestClassName.TestMethodName"` -- Build Core API: `dotnet build ConduitLLM.Http` -- Build Admin API: `dotnet build ConduitLLM.Admin` +**⚠️ CANONICAL DEVELOPMENT STARTUP:** +```bash +./scripts/dev/start-dev.sh +``` -### ⚠️ Production Testing Only +### Available Flags ```bash -# Only use for production-like testing, NOT for development -docker compose up -d +./scripts/dev/start-dev.sh # Standard startup +./scripts/dev/start-dev.sh --webadmin # Rebuild WebAdmin container +./scripts/dev/start-dev.sh --clean # Complete reset (removes all volumes) +./scripts/dev/start-dev.sh --build # Force rebuild with --no-cache +./scripts/dev/start-dev.sh --help # Show usage ``` -**Note**: Using `docker compose up -d` will create permission conflicts with development. If you accidentally use it, run `docker compose down --volumes --remove-orphans` before using `./scripts/start-dev.sh`. +**Flag Details:** +- `--webadmin`: Restarts WebAdmin container (fixes Next.js issues) +- `--clean`: Removes containers, volumes, node_modules, build artifacts +- `--build`: Rebuilds containers with `--no-cache` flag -## Docker Development Setup +## Available Services +After startup, these services are available: +- 🌐 **WebAdmin**: http://localhost:3000 (Next.js with hot reloading) +- 📚 **Gateway API Swagger**: http://localhost:5000/swagger +- 🔧 **Admin API Swagger**: http://localhost:5002/swagger +- 🐰 **RabbitMQ Management**: http://localhost:15672 (conduit/conduitpass) -### Starting Development Environment +## Quick Verification ```bash -# Always use the development script for local development -./scripts/start-dev.sh - -# If switching from production docker-compose: -docker compose down --volumes --remove-orphans -./scripts/start-dev.sh --clean +docker ps # Check running containers +docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f [service] ``` -### Verifying Services -```bash -# Check running containers -docker ps +--- -# View logs -docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f [service] -``` +# Development Environment + +## How Development Works + +### Key Features +- ✅ Node modules exist on HOST - direct npm command access +- ✅ WebAdmin directory mounted for hot reloading +- ✅ User ID mapping prevents permission issues (uses your UID/GID) +- ✅ Development containers use node:22-alpine directly +- ✅ Isolated .next directories - container manages its own build state ### Development vs Production | Aspect | Development (`start-dev.sh`) | Production (`docker compose up`) | |--------|------------------------------|----------------------------------| -| WebUI Container | `node:22-alpine` with mounted source | Built Next.js app in container | +| WebAdmin Container | `node:22-alpine` with mounted source | Built Next.js app in container | | Hot Reloading | ✅ Enabled via volume mounts | ❌ Static build | | User Permissions | Maps to host UID/GID | Runs as container user | | Node Modules | Shared with host | Container-only | -| Performance | Optimized for development | Optimized for production | -### How Development Environment Works +### Technical Implementation + +**Volume Mounting:** +- WebAdmin source: `./WebAdmin:/app/WebAdmin` +- SDKs: `./SDKs:/app/SDKs` +- Node modules accessible from both host and container + +**Permission Handling:** +1. Container starts as root +2. Installs su-exec (Alpine Linux user switching) +3. Fixes ownership to `${DOCKER_USER_ID}:${DOCKER_GROUP_ID}` +4. Switches to mapped user for all operations -**Volume Mounting**: WebUI source code is mounted directly into container, allowing hot reloading. +**Environment Variables:** +```bash +export DOCKER_USER_ID=$(id -u) +export DOCKER_GROUP_ID=$(id -g) +``` -**Permission Handling**: Container starts as root, fixes ownership to match host user (${DOCKER_USER_ID}:${DOCKER_GROUP_ID}), then switches to that user for all operations. +## Helper Commands -**Dependency Management**: Node modules are installed in mounted volumes, making them accessible from both host and container. +### dev-workflow.sh +```bash +./scripts/dev/dev-workflow.sh logs # WebAdmin logs (real-time) +./scripts/dev/dev-workflow.sh shell # Open shell in container +./scripts/dev/dev-workflow.sh lint-fix-webadmin # ESLint with --fix +./scripts/dev/dev-workflow.sh build-sdks # Build SDKs +./scripts/dev/dev-workflow.sh exec [command] # Execute custom command +``` -## Development Troubleshooting +### Other Helper Scripts +- `scripts/dev/fix-webadmin-errors.sh` - Automated TypeScript/ESLint fixes +- `scripts/dev/fix-sdk-errors.sh` - SDK TypeScript compilation fixes +- `scripts/dev/create-webadmin-key.sh` - Create virtual keys for testing +- `scripts/test/validate-eslint.sh` - Validate ESLint configuration -### Common Issues and Solutions +## Troubleshooting -#### Permission Denied Errors +### Permission Denied Errors ```bash # Symptom: npm EACCES errors, cannot write to node_modules -# Solution: Clean restart with proper permissions -./scripts/start-dev.sh --clean +./scripts/dev/start-dev.sh --clean ``` -#### After Adding New Packages +### After Adding New Packages ```bash -# When you add packages to package.json -# Restart WebUI container to install new dependencies -./scripts/start-dev.sh --webui +./scripts/dev/start-dev.sh --webadmin ``` -#### Container Conflicts +### Container Conflicts ```bash # Symptom: Containers already exist or port conflicts -# Solution: Stop all containers and restart docker compose down --volumes --remove-orphans -./scripts/start-dev.sh --clean +./scripts/dev/start-dev.sh --clean ``` -#### Next.js Build Issues +### Next.js Build Issues / Stale Builds ```bash -# Symptom: WebUI not updating, stale builds -# Solution: Restart WebUI container -./scripts/start-dev.sh --webui +./scripts/dev/start-dev.sh --webadmin ``` -#### WebUI Not Starting +### WebAdmin Not Starting ```bash -# Check container logs -docker compose -f docker-compose.yml -f docker-compose.dev.yml logs webui +# Check logs +docker compose -f docker-compose.yml -f docker-compose.dev.yml logs webadmin # Common causes: # 1. Port 3000 already in use @@ -157,241 +172,181 @@ docker compose -f docker-compose.yml -f docker-compose.dev.yml logs webui # 3. Node modules corruption (use --clean) ``` -#### Hot Reload Not Working +### Hot Reload Not Working ```bash # Verify file mounting -docker compose -f docker-compose.yml -f docker-compose.dev.yml exec webui ls -la /app/ConduitLLM.WebUI/ +docker compose -f docker-compose.yml -f docker-compose.dev.yml exec webadmin ls -la /app/WebAdmin/ -# Restart with clean build artifacts -rm -rf ConduitLLM.WebUI/.next -./scripts/start-dev.sh --webui +# Clean host build artifacts (container has isolated .next) +rm -rf WebAdmin/.next +./scripts/dev/start-dev.sh --webadmin ``` -### Development Services -After successful startup, these services are available: -- 🌐 **WebUI**: http://localhost:3000 (Next.js with hot reloading) -- 📚 **Core API Swagger**: http://localhost:5000/swagger -- 🔧 **Admin API Swagger**: http://localhost:5002/swagger -- 🐰 **RabbitMQ Management**: http://localhost:15672 (conduit/conduitpass) +--- + +# Build & Verification + +## ⚠️ CRITICAL: Always Verify Builds + +### WebAdmin Verification (SAFE) +**Never use `npm run build` - it breaks the container!** -### Development Helper Commands (dev-workflow.sh) ```bash -# Show WebUI logs in real-time -./scripts/dev-workflow.sh logs +cd WebAdmin +npm run lint # Check ESLint errors +npm run type-check # Verify TypeScript types +``` -# Open shell in WebUI container -./scripts/dev-workflow.sh shell +### Backend & SDK Build Commands +```bash +# Full solution +dotnet build -# Build WebUI in container -./scripts/dev-workflow.sh build-webui +# All tests +dotnet test -# Run ESLint with --fix -./scripts/dev-workflow.sh lint-fix-webui +# Specific test +dotnet test --filter "FullyQualifiedName=ConduitLLM.Tests.TestClassName.TestMethodName" -# Build SDKs -./scripts/dev-workflow.sh build-sdks +# Individual projects +dotnet build ConduitLLM.Gateway # Gateway API +dotnet build ConduitLLM.Admin # Admin API -# Execute any command in WebUI container -./scripts/dev-workflow.sh exec [command] +# SDKs +cd SDKs/Node/Admin && npm run build +cd SDKs/Node/Core && npm run build +cd SDKs/Node/Common && npm run build ``` -### Technical Implementation Notes +## Incremental Development Rules -#### How Permission Handling Works -The development environment uses user ID mapping to prevent permission issues: +1. **NEVER make more than 3-5 file changes without verifying** +2. **ALWAYS verify after ANY changes:** + - WebAdmin: `npm run lint` and `npm run type-check` ONLY + - Backend/SDKs: Use build commands above +3. **Fix ALL errors immediately** - do not accumulate technical debt +4. **Never commit code that doesn't verify cleanly** -1. **Container starts as root** - Allows initial setup and ownership changes -2. **Installs su-exec** - Lightweight tool for user switching in Alpine Linux -3. **Fixes volume ownership** - Changes ownership to match host user (${DOCKER_USER_ID}:${DOCKER_GROUP_ID}) -4. **Switches to host user** - All operations run as the mapped user +## Development Workflow -#### Volume Mounting Strategy -- WebUI source is mounted directly: `./ConduitLLM.WebUI:/app/ConduitLLM.WebUI` -- SDKs are mounted for development: `./SDKs:/app/SDKs` -- Node modules are accessible from both host and container -- No anonymous volumes that would block host access +1. Make changes to code +2. Verify immediately: + - **WebAdmin**: `npm run lint` && `npm run type-check` + - **Backend**: `dotnet build` + - **SDKs**: `npm run build` in SDK directory +3. Test changes: + - API changes: Test with Swagger UI or curl + - UI changes: Verify in browser with dev tools open +4. Clean up temporary files/scripts +5. Commit only verified code -#### Environment Variables Set by start-dev.sh -```bash -export DOCKER_USER_ID=$(id -u) # Your user ID -export DOCKER_GROUP_ID=$(id -g) # Your group ID +--- + +# Code Quality Standards + +## WebAdmin ESLint Strict Rules + +The WebAdmin uses **very strict ESLint rules** that cause build failures: + +### 1. Type Safety Rules +- `@typescript-eslint/no-unsafe-assignment` - Cannot assign `any`/`unknown` without explicit casting +- `@typescript-eslint/no-unsafe-argument` - Cannot pass `any`/`unknown` as arguments +- `@typescript-eslint/no-unsafe-member-access` - Cannot access properties on `any` types +- `@typescript-eslint/no-unsafe-return` - Cannot return `any` types + +### 2. Console Logging +- ✅ Only `console.warn` and `console.error` allowed +- ❌ `console.log` causes build failures +- Use `console.warn` for development debugging + +### 3. Nullish Coalescing +- Use `??` instead of `||` for default values +- Rule: `@typescript-eslint/prefer-nullish-coalescing` + +### 4. Type Casting Pattern +```typescript +// ❌ BAD - will fail ESLint +const data = event.data as MetricsData; + +// ✅ GOOD - proper type narrowing +const data = event.data as unknown as MetricsData; + +// ✅ BETTER - type guard +if (isMetricsData(event.data)) { + // event.data is now typed +} ``` -## Database Migrations - CRITICAL -**⚠️ ALWAYS READ [Database Migration Guide](docs/claude/database-migration-guide.md) BEFORE MAKING DATABASE CHANGES** -- We use PostgreSQL ONLY - no SQL Server syntax allowed -- Always run `./scripts/migrations/validate-postgresql-syntax.sh` after creating migrations -- Common mistake: Using `IsActive = 1` instead of `"IsActive" = true` - -## Build Verification - CRITICAL -**ALWAYS VERIFY BUILDS BEFORE COMPLETING WORK:** - -### ⚠️ WEBUI EXCEPTION - NEVER RUN NPM BUILD ⚠️ -**FORBIDDEN FOR WEBUI DEVELOPMENT:** -- ❌ `npm run build` - **WILL BREAK THE DEVELOPMENT CONTAINER** -- ❌ `cd ConduitLLM.WebUI && npm run build` - **WILL BREAK THE DEVELOPMENT CONTAINER** -- ❌ `./scripts/dev-workflow.sh build-webui` - **ONLY FOR PRODUCTION TESTING** - -**WEBUI VERIFICATION COMMANDS (SAFE FOR DEVELOPMENT):** -- ✅ `npm run lint` - Check ESLint errors -- ✅ `npm run type-check` - Verify TypeScript types -- ✅ Hot reloading in development container automatically validates code - -### Project-Specific Build Commands -- **WebUI**: **USE LINT AND TYPE-CHECK ONLY** (see above) -- **Core API**: `dotnet build ConduitLLM.Http` -- **Admin API**: `dotnet build ConduitLLM.Admin` -- **Admin SDK**: `cd SDKs/Node/Admin && npm run build` -- **Core SDK**: `cd SDKs/Node/Core && npm run build` -- **Common SDK**: `cd SDKs/Node/Common && npm run build` -- **Full Solution**: `dotnet build` - -### Incremental Development Rules -1. **NEVER make more than 3-5 file changes without verifying** -2. **ALWAYS run the appropriate verification command after ANY TypeScript/React changes** - - **WebUI**: `npm run lint` and `npm run type-check` ONLY - - **Backend/SDKs**: Use build commands listed above -3. **Fix ALL ESLint errors immediately - do not accumulate technical debt** -4. **Never commit code that doesn't verify cleanly** +## TypeScript/React Best Practices -### TypeScript/React Specific Rules -- When replacing `any` types, test immediately with `./scripts/fix-webui-errors.sh` or `./scripts/fix-sdk-errors.sh` +- Replace `any` types incrementally, verify immediately - Check existing error handling patterns before creating new ones -- Use small, incremental changes (1-3 files at a time) -- Follow established import patterns in the codebase -- Validate type changes before moving to next files - -### ESLint Error Prevention -- Configure stricter ESLint rules at the start of work +- Use small changes (1-3 files at a time) +- Follow established import patterns - Run `npm run lint` frequently during development -- Address warnings immediately, don't let them accumulate -- Use proper TypeScript patterns from existing codebase - -### WebUI ESLint Strict Rules - CRITICAL -The WebUI uses very strict ESLint rules that will cause build failures: - -1. **Type Safety Rules**: - - `@typescript-eslint/no-unsafe-assignment`: Cannot assign `any` or `unknown` types without explicit casting - - `@typescript-eslint/no-unsafe-argument`: Cannot pass `any` or `unknown` typed values as arguments - - `@typescript-eslint/no-unsafe-member-access`: Cannot access properties on `any` typed values - - `@typescript-eslint/no-unsafe-return`: Cannot return `any` typed values - -2. **Console Logging**: - - Only `console.warn` and `console.error` are allowed - - `console.log` will cause build failures - - Use `console.warn` for development debugging - -3. **Nullish Coalescing**: - - Use `??` instead of `||` for default values - - ESLint rule: `@typescript-eslint/prefer-nullish-coalescing` - -4. **Type Casting Pattern**: - ```typescript - // BAD - will fail ESLint - const data = event.data as MetricsData; - - // GOOD - proper type narrowing - const data = event.data as unknown as MetricsData; - // OR better - type guard - if (isMetricsData(event.data)) { - // event.data is now typed - } - ``` - -5. **Always Verify Before Committing**: - - **WebUI**: `npm run lint` and `npm run type-check` (NEVER `npm run build`) - - Fix ALL ESLint errors immediately - - The verification will fail with any ESLint errors +- Address warnings immediately -## Development Workflow -- After implementing features, always run: `dotnet build` to check for compilation errors -- **For WebUI changes**: Run `npm run lint` to check for ESLint errors -- **For TypeScript checks**: Run `npm run type-check` to verify types -- **❌ NEVER run `npm run build` for WebUI - it breaks the development container** -- Test your changes locally before committing -- When working with API changes, test with Swagger UI or curl -- For UI changes, verify in the browser with developer tools open -- Clean up temporary test files and scripts after completing features - -### Available Helper Scripts -- **fix-webui-errors.sh**: Automated fixes for common WebUI TypeScript/ESLint errors -- **fix-sdk-errors.sh**: Fixes SDK TypeScript compilation issues -- **validate-eslint.sh**: Validates ESLint configuration -- **dev-workflow.sh**: Helper commands for development tasks (see above) -- **create-webui-key.sh**: Creates virtual keys for WebUI testing -- **test-webui-connection.sh**: Tests WebUI connectivity - -### WebUI Development -You can run npm commands DIRECTLY on the host filesystem: -- ✅ `npm run lint` - Run ESLint -- ✅ `npm run type-check` - Check TypeScript types -- ✅ `cd SDKs/Node/Admin && npm run build` - Build SDKs -- ❌ **NEVER run `npm run build` for WebUI - breaks development container** - -The development environment shares node_modules between host and container. - -💡 **Development Notes**: -- The WebUI container runs Next.js dev server with hot-reloading -- Changes to source files are immediately reflected -- **DO NOT run production builds during development** -- To check types: `npm run type-check` (equivalent to `npx tsc --noEmit`) +## C# Code Style Guidelines -## Git Branching Rules -- **Protected branch**: `master` - Never push directly to master -- **Development branch**: `dev` - All development work should be pushed here -- **Feature branches**: Create from `dev` for new features -- **Pull requests**: Should target the `dev` branch -- **Release process**: `dev` is merged to `master` through controlled releases +### Naming Conventions +- Interfaces: Prefix with 'I' (e.g., `ILLMClient`) +- Async methods: Suffix with 'Async' +- Private fields: Prefix with underscore (`_logger`) +- Public members: PascalCase +- Parameters: camelCase -### Current Branch Status -- Main branch: `master` -- Active development: `dev` +### Formatting +- Indentation: 4 spaces +- Braces: Opening braces on new line (Allman style) +- Max line length: ~100 characters -## Code Style Guidelines -- **Naming**: - - Interfaces prefixed with 'I' (e.g., `ILLMClient`) - - Async methods suffixed with 'Async' - - Private fields prefixed with underscore (`_logger`) - - Use PascalCase for public members, camelCase for parameters -- **Formatting**: - - 4 spaces for indentation - - Opening braces on new line (Allman style) - - Max line length ~100 characters -- **Error Handling**: - - Use custom exception types inheriting from base exceptions - - Include contextual information in exception messages - - Use try/catch with appropriate logging -- **Testing**: - - Test methods follow pattern: `MethodName_Condition_ExpectedResult` - - One assertion per test is preferred - - Use Moq for mocking dependencies - -## Provider Architecture - CRITICAL -**⚠️ IMPORTANT**: The codebase supports multiple providers of the same type (e.g., multiple OpenAI configurations). Provider ID is the canonical identifier, not ProviderType. - -### Key Concepts: -- **Provider ID**: The canonical identifier for Provider records. Use this for lookups, relationships, and identification. -- **ProviderType**: Categorizes providers by their API type (OpenAI, Anthropic, etc.). Multiple providers can share the same ProviderType. -- **Provider Name**: User-facing display name. Can be changed and should not be used for identification. -- **ProviderKeyCredential**: Individual API keys for a provider. Supports multiple keys per provider for load balancing and failover. - -### Architecture: -- **Provider Entity**: Represents a provider instance (e.g., "Production OpenAI", "Dev Azure OpenAI") -- **ProviderKeyCredential Entity**: Individual API keys with ProviderAccountGroup for external account separation -- **ModelProviderMapping**: Links model aliases to Provider.Id (NOT ProviderType!) -- **ModelCost**: Flexible cost configurations that can apply to multiple models via ModelCostMapping +### Error Handling +- Use custom exception types inheriting from base exceptions +- Include contextual information in exception messages +- Use try/catch with appropriate logging + +### Testing +- Test method pattern: `MethodName_Condition_ExpectedResult` +- One assertion per test (preferred) +- Use Moq for mocking dependencies + +--- + +# Architecture Essentials + +## Database Migrations -### Migration Notes: -- **ProviderType enum**: Used for categorization, stored as integers in database -- **Provider instances**: Multiple providers of the same type can exist (e.g., multiple OpenAI configs) -- **Backward compatibility**: Some legacy properties may be marked `[Obsolete]` +**⚠️ CRITICAL: PostgreSQL Syntax ONLY** -### Documentation: -- See `/docs/architecture/provider-multi-instance.md` for detailed provider architecture -- See `/docs/architecture/model-cost-mapping.md` for cost configuration details -- See `/docs/architecture/provider-system-analysis.md` for system analysis +### Requirements +- Use standard EF Core workflow: `dotnet ef migrations add` → `dotnet ef database update` +- PostgreSQL syntax only (double quotes for identifiers, `true`/`false` booleans) +- Test tooling first: `dotnet ef --version` +- See `docs/architecture/patterns/repository-and-data-access.md` for best practices -### Available Provider Types: +### Common Mistakes +- ❌ SQL Server syntax: `IsActive = 1` +- ✅ PostgreSQL syntax: `"IsActive" = true` +- ❌ Single quotes for identifiers +- ✅ Double quotes for identifiers + +## Provider Architecture + +**⚠️ IMPORTANT**: The codebase supports multiple providers of the same type (e.g., multiple OpenAI configurations). **Provider ID is the canonical identifier, not ProviderType.** + +### Key Concepts +- **Provider ID**: Canonical identifier for Provider records (use for lookups, relationships, identification) +- **ProviderType**: Categorizes by API type (OpenAI, Anthropic, etc.) - multiple providers can share the same type +- **Provider Name**: User-facing display name (can change, don't use for identification) +- **ProviderKeyCredential**: Individual API keys per provider (supports load balancing/failover) + +### Architecture +- **Provider Entity**: Instance (e.g., "Production OpenAI", "Dev Azure OpenAI") +- **ProviderKeyCredential Entity**: API keys with ProviderAccountGroup for account separation +- **ModelProviderMapping**: Links model aliases to Provider.Id (NOT ProviderType!) +- **ModelCost**: Flexible cost configs via ModelCostMapping + +### Available Provider Types ```csharp public enum ProviderType { @@ -402,79 +357,163 @@ public enum ProviderType OpenAICompatible = 5, MiniMax = 6, Ultravox = 7, - ElevenLabs = 8, // Audio provider - Cerebras = 9, // High-performance inference - SambaNova = 10 // Ultra-fast inference + ElevenLabs = 8, // Audio provider + Cerebras = 9, // High-performance inference + SambaNova = 10 // Ultra-fast inference } ``` -## Detailed Documentation - -For comprehensive documentation on specific topics, see: - -- **[Database Migration Guide](docs/claude/database-migration-guide.md)** - PostgreSQL migration requirements and validation -- **[XML Documentation Standards](docs/claude/xml-documentation-standards.md)** - Comprehensive XML documentation requirements -- **[Media Storage Configuration](docs/claude/media-storage-configuration.md)** - S3/CDN setup, Docker SignalR configuration -- **[Event-Driven Architecture](docs/claude/event-driven-architecture.md)** - MassTransit events, domain events -- **[SignalR Configuration](docs/claude/signalr-configuration.md)** - Real-time updates, Redis backplane -- **[RabbitMQ High-Throughput](docs/claude/rabbitmq-high-throughput.md)** - Production scaling configuration -- **[Provider Models](docs/claude/provider-models.md)** - Supported models by provider -- **[R2 Health Check](docs/claude/r2-health-check.md)** - Cloudflare R2 health monitoring -- **[SDK Generation Workflow](docs/claude/sdk-generation-workflow.md)** - SDK generation from OpenAPI specs -- **[Batch Cache Invalidation](docs/claude/batch-cache-invalidation.md)** - Cache invalidation batching -- **[Workflow Concurrency Strategy](docs/claude/workflow-concurrency-strategy.md)** - Concurrent execution patterns - -## Key Points from Detailed Docs - -### Media Storage -- Development defaults to S3-compatible storage (configure in .env) -- Production requires S3-compatible storage (AWS S3, Cloudflare R2) -- **WARNING**: Media files are not automatically cleaned up when virtual keys are deleted - -### Cloudflare R2 Specific Configuration -- **Automatic Detection**: The system automatically detects R2 based on the service URL -- **Optimized Settings**: When R2 is detected, multipart uploads use 10MB chunks (vs 5MB default) -- **CORS Configuration**: R2 may require manual CORS setup in the Cloudflare dashboard -- **Public Access**: Enable public access in R2 dashboard for CDN functionality -- **Benefits**: R2 offers free egress bandwidth, making it cost-effective for media delivery - -### Event-Driven Architecture -- Uses MassTransit for event processing -- Supports in-memory (dev) or RabbitMQ (production) transport +**Documentation:** +- `docs/architecture/provider-system/provider-architecture.md` - Detailed architecture +- `docs/architecture/provider-system/model-and-cost-mapping.md` - Cost configuration +- `docs/architecture/provider-system/provider-system-analysis.md` - System analysis + +## Security & Authentication + +### WebAdmin Authentication +- **Human admins**: Authenticate through Clerk (not password-based) +- **Backend service-to-service**: Uses `CONDUIT_API_TO_API_BACKEND_AUTH_KEY` + +### Backend Authentication Key +**CONDUIT_API_TO_API_BACKEND_AUTH_KEY**: +- Used by WebAdmin backend to authenticate with Gateway API and Admin API +- Server-to-server communication only +- NOT for end-users or client applications +- Configured on WebAdmin service + +## WebAdmin API Architecture + +**The WebAdmin has only 3 API routes** - relies on client-side SDK usage with ephemeral keys: +- `/api/health` - Health check endpoint +- `/api/auth/ephemeral-key` - Generate virtual keys for Gateway API access +- `/api/auth/ephemeral-master-key` - Generate master keys for Admin API access + +### When Writing New API Routes +- ✅ Use `getServerAdminClient()` for Admin API access +- ✅ Use `await getServerCoreClient()` for Gateway API access (async!) +- ✅ Wrap all SDK calls in `try/catch` with `handleSDKError(error)` +- ✅ Return `NextResponse.json(data)` for responses +- ✅ Validate request input before passing to SDK +- ❌ **Never** create SDK clients directly with `new ConduitAdminClient()` +- ❌ **Never** expose master keys to clients + +**Full guide:** `docs/development/API-PATTERNS-BEST-PRACTICES.md` + +## Media Storage & Cleanup + +**⚠️ CRITICAL**: Proper cleanup configuration required to prevent unbounded storage costs + +### Configuration +- Development: S3-compatible storage (configure in .env) +- Production: AWS S3 or Cloudflare R2 +- **MUST** configure storage provider in Admin API for automatic cleanup +- See `docs/CRITICAL-Media-Cleanup-Configuration.md` + +### Cloudflare R2 Specifics +- Automatic detection based on service URL +- 10MB multipart upload chunks (vs 5MB default) +- May require manual CORS setup in dashboard +- Enable public access in R2 dashboard for CDN +- Free egress bandwidth + +## Event-Driven Architecture + +- **MassTransit** for event processing with RabbitMQ +- Supports in-memory (dev) or RabbitMQ (production) - Events ensure cache consistency and eliminate race conditions -- Virtual Key events are partitioned by key ID for ordered processing - -### Real-Time Updates -- SignalR provides real-time navigation state updates -- Supports Redis backplane for horizontal scaling -- Falls back to polling if WebSocket connection fails -- Three hubs: navigation-state, video-generation, image-generation - -### High-Throughput Configuration -- RabbitMQ supports 1,000+ async tasks per minute -- Optimized settings: 25 prefetch, 30 partitions, 50 concurrent messages -- HTTP client connection pooling: 50 connections per server -- Circuit breakers and rate limiting prevent overload - -# CRITICAL SAFETY SECTION - READ FIRST -## Commands That WILL Break Development and Waste Time - -### ❌ FORBIDDEN WEBUI COMMANDS -These commands will break the development container and force a 5+ minute restart: -- `npm run build` (anywhere in WebUI directory) -- `cd ConduitLLM.WebUI && npm run build` -- `./scripts/dev-workflow.sh build-webui` (production only) - -### ✅ SAFE WEBUI COMMANDS -Use these instead for WebUI verification: -- `npm run lint` -- `npm run type-check` +- Virtual Key events partitioned by key ID for ordered processing -### ❌ FORBIDDEN DEVELOPMENT COMMANDS -- `docker compose up` for development (use `./scripts/start-dev.sh`) +**See:** `docs/architecture/media-generation/async-media-generation.md` -**If you run any forbidden command, you will:** -1. Break the development environment -2. Force the human to restart with `--clean` -3. Waste 5+ minutes of their time -4. Ignore explicit instructions \ No newline at end of file +## Real-Time Updates + +- **SignalR** provides real-time updates via WebSockets +- Redis backplane for horizontal scaling +- Falls back to polling if WebSocket fails +- Hubs: navigation-state, video-generation, image-generation + +**See:** `docs/architecture/real-time/streaming-and-websockets.md` + +## High-Throughput Configuration + +- RabbitMQ: 1,000+ async tasks per minute +- Settings: 25 prefetch, 30 partitions, 50 concurrent messages +- HTTP client pooling: 50 connections per server +- Circuit breakers and rate limiting + +**See:** `docs/operations/infrastructure/rabbitmq-scaling.md` + +--- + +# Documentation Index + +## Core Development Guides +- **[API Patterns & Best Practices](docs/development/API-PATTERNS-BEST-PRACTICES.md)** - WebAdmin API patterns, SDK usage, error handling +- **[LLM Client Factory Guide](docs/development/llm-client-factory-guide.md)** - Provider client creation patterns +- **[Development Documentation](docs/development/README.md)** - Development guides index + +## Architecture Documentation +- **[Architecture Overview](docs/architecture/README.md)** - Complete architecture index +- **[Provider System](docs/architecture/provider-system/provider-architecture.md)** - Provider design, multi-instance support +- **[Model & Cost Mapping](docs/architecture/provider-system/model-and-cost-mapping.md)** - Cost tracking details +- **[Streaming & WebSockets](docs/architecture/real-time/streaming-and-websockets.md)** - Real-time communication, SSE +- **[Webhook Delivery](docs/architecture/real-time/webhook-delivery.md)** - Distributed delivery, circuit breakers +- **[Async Media Generation](docs/architecture/media-generation/async-media-generation.md)** - Event-driven image/video +- **[Background Services](docs/architecture/patterns/background-services-and-workers.md)** - Worker patterns, distributed locking +- **[Repository & Data Access](docs/architecture/patterns/repository-and-data-access.md)** - EF Core best practices +- **[DTO Guidelines](docs/architecture/data-transfer/dto-guidelines.md)** - Data transfer patterns +- **[Scaling Architecture](docs/architecture/infrastructure/scaling-architecture.md)** - 10,000+ concurrent sessions + +## Operations & Deployment +- **[Operations Documentation](docs/operations/README.md)** - Operations index +- **[SignalR Configuration](docs/operations/signalr/configuration.md)** - Real-time updates, Redis backplane +- **[RabbitMQ Scaling](docs/operations/infrastructure/rabbitmq-scaling.md)** - 1,000+ tasks/min configuration +- **[Redis Resilience](docs/operations/infrastructure/redis-resilience.md)** - Configuration and failover +- **[PostgreSQL Scaling](docs/operations/infrastructure/postgresql-scaling.md)** - Database scaling +- **[HTTP Connection Pooling](docs/operations/infrastructure/http-connection-pooling.md)** - Connection optimization +- **[Provider Health Monitoring](docs/operations/providers/health-monitoring.md)** - Provider status tracking +- **[Provider Usage Mappings](docs/operations/providers/usage-mappings.md)** - Usage tracking config +- **[Deployment Configuration](docs/operations/deployment/DEPLOYMENT-CONFIGURATION.md)** - Production guide +- **[Docker Optimization](docs/operations/deployment/docker-optimization.md)** - Container optimization + +## Media & Storage +- **[Media Cleanup Configuration](docs/CRITICAL-Media-Cleanup-Configuration.md)** - ⚠️ CRITICAL - S3/R2 cleanup requirements + +## API Integration Guides +- **[API Guides Index](docs/api-guides/README.md)** - API integration documentation +- **[Gateway API Getting Started](docs/api-guides/core/getting-started.md)** - Gateway API usage +- **[Admin API Getting Started](docs/api-guides/admin/getting-started.md)** - Admin API usage +- **[SignalR Getting Started](docs/api-guides/signalr/getting-started.md)** - Real-time integration +- **[SDK Best Practices](docs/api-guides/sdk/best-practices.md)** - SDK usage patterns +- **[Next.js Integration](docs/api-guides/sdk/nextjs-integration.md)** - WebAdmin SDK integration + +--- + +# Repository & Collaboration + +## Git Branching Rules + +- **Protected branch**: `master` - Never push directly +- **Development branch**: `dev` - All development work goes here +- **Feature branches**: Create from `dev` +- **Pull requests**: Target the `dev` branch +- **Release process**: `dev` merged to `master` through controlled releases + +### Current Branch Status +- Main branch: `master` +- Active development: `dev` + +## Collaboration Guidelines + +- **Challenge and question**: Don't immediately agree with suboptimal/unclear requests +- **Push back constructively**: Suggest better alternatives with clear reasoning +- **Think critically**: Consider edge cases, performance, maintainability, best practices +- **Seek clarification**: Ask follow-up questions when requirements are ambiguous +- **Propose improvements**: Suggest better patterns, more robust solutions +- **Be a thoughtful collaborator**: Act as a good teammate improving project quality + +## Repository Information + +- **GitHub Repository**: knnlabs/Conduit +- **Issues URL**: https://github.com/knnlabs/Conduit/issues +- **Pull Requests URL**: https://github.com/knnlabs/Conduit/pulls diff --git a/Conduit.sln b/Conduit.sln index 3f89625d8..5871b23fa 100644 --- a/Conduit.sln +++ b/Conduit.sln @@ -3,19 +3,19 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Core", "ConduitLLM.Core\ConduitLLM.Core.csproj", "{F7E17FAA-F116-41DC-8242-58334599B97A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Core", "Shared\ConduitLLM.Core\ConduitLLM.Core.csproj", "{F7E17FAA-F116-41DC-8242-58334599B97A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Providers", "ConduitLLM.Providers\ConduitLLM.Providers.csproj", "{F609AD6B-7FCD-46C2-BA05-248196CB3018}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Providers", "Shared\ConduitLLM.Providers\ConduitLLM.Providers.csproj", "{F609AD6B-7FCD-46C2-BA05-248196CB3018}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Configuration", "ConduitLLM.Configuration\ConduitLLM.Configuration.csproj", "{9609B6C6-B67D-4747-BC26-16C0D4506982}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Configuration", "Shared\ConduitLLM.Configuration\ConduitLLM.Configuration.csproj", "{9609B6C6-B67D-4747-BC26-16C0D4506982}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Http", "ConduitLLM.Http\ConduitLLM.Http.csproj", "{28E1AD04-67E6-43B9-9553-E61ED4F49965}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Gateway", "Services\ConduitLLM.Gateway\ConduitLLM.Gateway.csproj", "{28E1AD04-67E6-43B9-9553-E61ED4F49965}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Tests", "ConduitLLM.Tests\ConduitLLM.Tests.csproj", "{DEA5022E-5467-46C6-9942-F8082AE61F6C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Tests", "Tests\ConduitLLM.Tests\ConduitLLM.Tests.csproj", "{DEA5022E-5467-46C6-9942-F8082AE61F6C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Admin", "ConduitLLM.Admin\ConduitLLM.Admin.csproj", "{A54C7B18-2E33-41D9-8BC0-5F93B95C64CA}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Admin", "Services\ConduitLLM.Admin\ConduitLLM.Admin.csproj", "{A54C7B18-2E33-41D9-8BC0-5F93B95C64CA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Security", "ConduitLLM.Security\ConduitLLM.Security.csproj", "{159C66C3-96A6-42AA-BE61-087482810F76}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConduitLLM.Security", "Shared\ConduitLLM.Security\ConduitLLM.Security.csproj", "{159C66C3-96A6-42AA-BE61-087482810F76}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/ConduitLLM.Admin/ConduitLLM.Admin.csproj b/ConduitLLM.Admin/ConduitLLM.Admin.csproj deleted file mode 100644 index 951d1aba2..000000000 --- a/ConduitLLM.Admin/ConduitLLM.Admin.csproj +++ /dev/null @@ -1,58 +0,0 @@ - - - - net9.0 - enable - enable - true - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file 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/ConfigurationController.cs b/ConduitLLM.Admin/Controllers/ConfigurationController.cs deleted file mode 100644 index bc5fed121..000000000 --- a/ConduitLLM.Admin/Controllers/ConfigurationController.cs +++ /dev/null @@ -1,374 +0,0 @@ -using ConduitLLM.Configuration; -using Microsoft.AspNetCore.Authorization; -using ConduitLLM.Configuration.DTOs; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Memory; -using ConduitLLM.Admin.Services; -using ConduitLLM.Configuration.DTOs.Cache; - -namespace ConduitLLM.Admin.Controllers -{ - /// - /// Controller for managing system configuration including routing and caching. - /// - [ApiController] - [Route("api/config")] - [Authorize(Policy = "MasterKeyPolicy")] - public class ConfigurationController : ControllerBase - { - private readonly IDbContextFactory _dbContextFactory; - private readonly ILogger _logger; - private readonly IMemoryCache _cache; - private readonly IConfiguration _configuration; - private readonly ICacheManagementService _cacheManagementService; - - /// - /// Initializes a new instance of the class. - /// - /// Database context factory. - /// Logger instance. - /// Memory cache. - /// Application configuration. - /// Service for cache maintenance operations. - public ConfigurationController( - IDbContextFactory dbContextFactory, - ILogger logger, - IMemoryCache cache, - IConfiguration configuration, - ICacheManagementService cacheManagementService) - { - _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _cache = cache ?? throw new ArgumentNullException(nameof(cache)); - _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _cacheManagementService = cacheManagementService ?? throw new ArgumentNullException(nameof(cacheManagementService)); - } - - /// - /// Gets routing configuration and rules. - /// - /// Cancellation token. - /// Routing configuration data. - [HttpGet("routing")] - public async Task GetRoutingConfig(CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - // Get model-to-provider mappings - var modelMappings = await dbContext.ModelProviderMappings - .Include(m => m.Provider) - .Select(m => new - { - Id = m.Id, - ModelAlias = m.ModelAlias, - ProviderModelId = m.ProviderModelId, - IsEnabled = m.IsEnabled, - Provider = new - { - Id = m.Provider.Id, - Name = m.Provider.ProviderName, - Type = m.Provider.ProviderType, - IsEnabled = m.Provider.IsEnabled - } - }) - .ToListAsync(cancellationToken); - - // Get load balancing configuration - var loadBalancers = new List - { - new - { - Id = "primary", - Name = "Primary Load Balancer", - Algorithm = _configuration["LoadBalancing:Algorithm"] ?? "round-robin", - HealthCheckInterval = 30, - FailoverThreshold = 3, - Endpoints = await GetProviderEndpoints(dbContext, cancellationToken) - } - }; - - - // Get routing statistics - var routingStats = await GetRoutingStatistics(dbContext, cancellationToken); - - return Ok(new - { - Timestamp = DateTime.UtcNow, - RoutingRules = modelMappings, - LoadBalancers = loadBalancers, - Statistics = routingStats, - Configuration = new - { - EnableFailover = _configuration.GetValue("Routing:EnableFailover", true), - EnableLoadBalancing = _configuration.GetValue("Routing:EnableLoadBalancing", true), - RequestTimeout = _configuration.GetValue("Routing:RequestTimeoutSeconds", 30), - CircuitBreakerThreshold = _configuration.GetValue("Routing:CircuitBreakerThreshold", 5) - } - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to retrieve routing configuration"); - return StatusCode(500, new { error = "Failed to retrieve routing configuration", message = ex.Message }); - } - } - - /// - /// Gets caching configuration and statistics. - /// - /// Cancellation token. - /// Caching configuration data. - [HttpGet("caching")] - public async Task GetCachingConfig(CancellationToken cancellationToken = default) - { - try - { - var configuration = await _cacheManagementService.GetConfigurationAsync(cancellationToken); - return Ok(configuration); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to retrieve caching configuration"); - return StatusCode(500, new { error = "Failed to retrieve caching configuration", message = ex.Message }); - } - } - - - /// - /// Updates caching configuration. - /// - /// Updated caching configuration. - /// Cancellation token. - /// Success response. - [HttpPut("caching")] - public async Task UpdateCachingConfig([FromBody] UpdateCacheConfigDto config, CancellationToken cancellationToken = default) - { - try - { - await _cacheManagementService.UpdateConfigurationAsync(config, cancellationToken); - return Ok(new { message = "Caching configuration updated successfully" }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to update caching configuration"); - return StatusCode(500, new { error = "Failed to update caching configuration", message = ex.Message }); - } - } - - /// - /// Clears specific cache by ID. - /// - /// Cache policy ID. - /// Cancellation token. - /// Success response. - [HttpPost("caching/{cacheId}/clear")] - public async Task ClearCache(string cacheId, CancellationToken cancellationToken = default) - { - try - { - await _cacheManagementService.ClearCacheAsync(cacheId, cancellationToken); - return Ok(new { message = $"Cache '{cacheId}' cleared successfully" }); - } - catch (ArgumentException ex) - { - return BadRequest(new { error = ex.Message }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to clear cache {CacheId}", cacheId); - return StatusCode(500, new { error = "Failed to clear cache", message = ex.Message }); - } - } - - /// - /// Gets cache statistics for all regions or a specific region. - /// - /// Optional region ID. - /// Cancellation token. - /// Cache statistics. - [HttpGet("caching/statistics")] - public async Task GetCacheStatistics([FromQuery] string? regionId = null, CancellationToken cancellationToken = default) - { - try - { - var statistics = await _cacheManagementService.GetStatisticsAsync(regionId, cancellationToken); - return Ok(statistics); - } - catch (ArgumentException ex) - { - return BadRequest(new { error = ex.Message }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to get cache statistics"); - return StatusCode(500, new { error = "Failed to get cache statistics", message = ex.Message }); - } - } - - /// - /// Lists all cache regions. - /// - /// Cancellation token. - /// List of cache regions. - [HttpGet("caching/regions")] - public async Task GetCacheRegions(CancellationToken cancellationToken = default) - { - try - { - var configuration = await _cacheManagementService.GetConfigurationAsync(cancellationToken); - return Ok(new - { - Regions = configuration.CacheRegions, - Timestamp = DateTime.UtcNow - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to get cache regions"); - return StatusCode(500, new { error = "Failed to get cache regions", message = ex.Message }); - } - } - - /// - /// Gets entries from a specific cache region. - /// - /// Region ID. - /// Number of entries to skip. - /// Number of entries to return. - /// Cancellation token. - /// Cache entries. - [HttpGet("caching/{regionId}/entries")] - public async Task GetCacheEntries(string regionId, [FromQuery] int skip = 0, [FromQuery] int take = 100, CancellationToken cancellationToken = default) - { - try - { - if (take > 1000) - { - return BadRequest(new ErrorResponseDto("Cannot retrieve more than 1000 entries at once")); - } - - var entries = await _cacheManagementService.GetEntriesAsync(regionId, skip, take, cancellationToken); - return Ok(entries); - } - catch (ArgumentException ex) - { - return BadRequest(new { error = ex.Message }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to get cache entries for region {RegionId}", regionId); - return StatusCode(500, new { error = "Failed to get cache entries", message = ex.Message }); - } - } - - /// - /// Forces a refresh of cache entries in a region. - /// - /// Region ID. - /// Optional specific key to refresh. - /// Cancellation token. - /// Success response. - [HttpPost("caching/{regionId}/refresh")] - public async Task RefreshCache(string regionId, [FromQuery] string? key = null, CancellationToken cancellationToken = default) - { - try - { - await _cacheManagementService.RefreshCacheAsync(regionId, key, cancellationToken); - var message = string.IsNullOrEmpty(key) - ? $"Cache region '{regionId}' refreshed successfully" - : $"Cache key '{key}' in region '{regionId}' refreshed successfully"; - return Ok(new { message }); - } - catch (ArgumentException ex) - { - return BadRequest(new { error = ex.Message }); - } - catch (KeyNotFoundException ex) - { - return NotFound(new { error = ex.Message }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to refresh cache for region {RegionId}", regionId); - return StatusCode(500, new { error = "Failed to refresh cache", message = ex.Message }); - } - } - - /// - /// Updates the policy for a specific cache region. - /// - /// Region ID. - /// Policy update details. - /// Cancellation token. - /// Success response. - [HttpPut("caching/{regionId}/policy")] - public async Task UpdateCachePolicy(string regionId, [FromBody] UpdateCachePolicyDto policyUpdate, CancellationToken cancellationToken = default) - { - try - { - await _cacheManagementService.UpdatePolicyAsync(regionId, policyUpdate, cancellationToken); - return Ok(new { message = $"Cache policy for region '{regionId}' updated successfully" }); - } - catch (ArgumentException ex) - { - return BadRequest(new { error = ex.Message }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to update cache policy for region {RegionId}", regionId); - return StatusCode(500, new { error = "Failed to update cache policy", message = ex.Message }); - } - } - - private async Task> GetProviderEndpoints(ConduitDbContext dbContext, CancellationToken cancellationToken) - { - var providers = await dbContext.Providers - .Where(p => p.IsEnabled) - .Select(p => new - { - p.Id, - p.ProviderName, - p.ProviderType, - p.BaseUrl - }) - .ToListAsync(cancellationToken); - - return providers.Select(p => (object)new - { - Id = p.Id, - Name = p.ProviderName, - Type = p.ProviderType.ToString(), - Url = p.BaseUrl ?? $"https://api.{p.ProviderType.ToString().ToLower()}.com", - Weight = 1 - }).ToList(); - } - - private async Task GetRoutingStatistics(ConduitDbContext dbContext, CancellationToken cancellationToken) - { - var oneDayAgo = DateTime.UtcNow.AddDays(-1); - - var stats = await dbContext.RequestLogs - .Where(r => r.Timestamp >= oneDayAgo) - .GroupBy(r => r.ModelName) - .Select(g => new - { - Provider = g.Key, - RequestCount = g.Count(), - SuccessRate = g.Count(r => r.StatusCode < 400) * 100.0 / g.Count(), - AvgLatency = g.Average(r => r.ResponseTimeMs) - }) - .ToListAsync(cancellationToken); - - return new - { - TotalRequests = stats.Sum(s => s.RequestCount), - ProviderDistribution = stats - }; - } - - } - -} diff --git a/ConduitLLM.Admin/Controllers/GlobalSettingsController.cs b/ConduitLLM.Admin/Controllers/GlobalSettingsController.cs deleted file mode 100644 index fbb290281..000000000 --- a/ConduitLLM.Admin/Controllers/GlobalSettingsController.cs +++ /dev/null @@ -1,279 +0,0 @@ -using ConduitLLM.Admin.Interfaces; -using ConduitLLM.Configuration.DTOs; - -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace ConduitLLM.Admin.Controllers -{ - /// - /// Controller for managing global settings - /// - [ApiController] - [Route("api/[controller]")] - [Authorize(Policy = "MasterKeyPolicy")] - public class GlobalSettingsController : ControllerBase - { - private readonly IAdminGlobalSettingService _globalSettingService; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the GlobalSettingsController - /// - /// The global setting service - /// The logger - public GlobalSettingsController( - IAdminGlobalSettingService globalSettingService, - ILogger logger) - { - _globalSettingService = globalSettingService ?? throw new ArgumentNullException(nameof(globalSettingService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Gets all global settings - /// - /// List of all global settings - [HttpGet] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetAllSettings() - { - try - { - var settings = await _globalSettingService.GetAllSettingsAsync(); - return Ok(settings); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting all global settings"); - return StatusCode(StatusCodes.Status500InternalServerError, "An unexpected error occurred."); - } - } - - /// - /// Gets a global setting by ID - /// - /// The ID of the setting to get - /// The global setting - [HttpGet("{id}")] - [ProducesResponseType(typeof(GlobalSettingDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetSettingById(int id) - { - try - { - var setting = await _globalSettingService.GetSettingByIdAsync(id); - - if (setting == null) - { - return NotFound(new ErrorResponseDto("Global setting not found")); - } - - return Ok(setting); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting global setting with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An unexpected error occurred."); - } - } - - /// - /// Gets a global setting by key - /// - /// The key of the setting to get - /// The global setting - [HttpGet("by-key/{key}")] - [ProducesResponseType(typeof(GlobalSettingDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetSettingByKey(string key) - { - try - { - var setting = await _globalSettingService.GetSettingByKeyAsync(key); - - if (setting == null) - { - return NotFound(new ErrorResponseDto("Global setting not found")); - } - - return Ok(setting); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting global setting with key {Key}", key.Replace(Environment.NewLine, "")); - return StatusCode(StatusCodes.Status500InternalServerError, "An unexpected error occurred."); - } - } - - /// - /// Creates a new global setting - /// - /// The setting to create - /// The created setting - [HttpPost] - [ProducesResponseType(typeof(GlobalSettingDto), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task CreateSetting([FromBody] CreateGlobalSettingDto setting) - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - try - { - var createdSetting = await _globalSettingService.CreateSettingAsync(setting); - return CreatedAtAction(nameof(GetSettingById), new { id = createdSetting.Id }, createdSetting); - } - catch (InvalidOperationException ex) - { - _logger.LogWarning(ex, "Invalid operation when creating global setting"); - return BadRequest(ex.Message); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating global setting"); - return StatusCode(StatusCodes.Status500InternalServerError, "An unexpected error occurred."); - } - } - - /// - /// Updates an existing global setting - /// - /// The ID of the setting to update - /// The updated setting data - /// No content if successful - [HttpPut("{id}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task UpdateSetting(int id, [FromBody] UpdateGlobalSettingDto setting) - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - // Ensure ID in route matches ID in body - if (id != setting.Id) - { - return BadRequest("ID in route must match ID in body"); - } - - try - { - var success = await _globalSettingService.UpdateSettingAsync(setting); - - if (!success) - { - return NotFound(new ErrorResponseDto("Global setting not found")); - } - - return NoContent(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating global setting with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An unexpected error occurred."); - } - } - - /// - /// Updates or creates a global setting by key - /// - /// The setting data with key, value, and optional description - /// No content if successful - [HttpPut("by-key")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task UpdateSettingByKey([FromBody] UpdateGlobalSettingByKeyDto setting) - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - try - { - var success = await _globalSettingService.UpdateSettingByKeyAsync(setting); - - if (!success) - { - return StatusCode(StatusCodes.Status500InternalServerError, "Failed to update or create global setting"); - } - - return NoContent(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating global setting with key {Key}", setting.Key.Replace(Environment.NewLine, "")); - return StatusCode(StatusCodes.Status500InternalServerError, "An unexpected error occurred."); - } - } - - /// - /// Deletes a global setting - /// - /// The ID of the setting to delete - /// No content if successful - [HttpDelete("{id}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task DeleteSetting(int id) - { - try - { - var success = await _globalSettingService.DeleteSettingAsync(id); - - if (!success) - { - return NotFound(new ErrorResponseDto("Global setting not found")); - } - - return NoContent(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting global setting with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An unexpected error occurred."); - } - } - - /// - /// Deletes a global setting by key - /// - /// The key of the setting to delete - /// No content if successful - [HttpDelete("by-key/{key}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task DeleteSettingByKey(string key) - { - try - { - var success = await _globalSettingService.DeleteSettingByKeyAsync(key); - - if (!success) - { - return NotFound(new ErrorResponseDto("Global setting not found")); - } - - return NoContent(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting global setting with key {Key}", key.Replace(Environment.NewLine, "")); - return StatusCode(StatusCodes.Status500InternalServerError, "An unexpected error occurred."); - } - } - } -} diff --git a/ConduitLLM.Admin/Controllers/ModelCapabilitiesController.cs b/ConduitLLM.Admin/Controllers/ModelCapabilitiesController.cs deleted file mode 100644 index cb94b7261..000000000 --- a/ConduitLLM.Admin/Controllers/ModelCapabilitiesController.cs +++ /dev/null @@ -1,309 +0,0 @@ -using ConduitLLM.Admin.Models.ModelCapabilities; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.Repositories; - -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace ConduitLLM.Admin.Controllers -{ - /// - /// Controller for managing ModelCapabilities entities - /// - [ApiController] - [Route("api/[controller]")] - [Authorize(Policy = "MasterKeyPolicy")] - public class ModelCapabilitiesController : ControllerBase - { - private readonly IModelCapabilitiesRepository _repository; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the ModelCapabilitiesController - /// - public ModelCapabilitiesController( - IModelCapabilitiesRepository repository, - ILogger logger) - { - _repository = repository ?? throw new ArgumentNullException(nameof(repository)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Gets all model capabilities - /// - /// List of all model capabilities - [HttpGet] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetAll() - { - try - { - var capabilities = await _repository.GetAllAsync(); - var dtos = capabilities.Select(c => MapToDto(c)); - return Ok(dtos); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting all model capabilities"); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while retrieving model capabilities"); - } - } - - /// - /// Gets a specific model capabilities by ID - /// - /// The capabilities ID - /// The model capabilities - [HttpGet("{id}")] - [ProducesResponseType(typeof(CapabilitiesDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetById(int id) - { - try - { - var capabilities = await _repository.GetByIdAsync(id); - if (capabilities == null) - { - return NotFound($"Model capabilities with ID {id} not found"); - } - - return Ok(MapToDto(capabilities)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting model capabilities with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while retrieving the model capabilities"); - } - } - - /// - /// Gets models using specific capabilities - /// - /// The capabilities ID - /// List of models using the capabilities - [HttpGet("{id}/models")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetModelsUsingCapabilities(int id) - { - try - { - var models = await _repository.GetModelsUsingCapabilitiesAsync(id); - if (models == null) - { - return NotFound($"Model capabilities with ID {id} not found"); - } - - var dtos = models.Select(m => new CapabilitiesSimpleModelDto - { - Id = m.Id, - Name = m.Name, - Version = m.Version, - IsActive = m.IsActive - }); - - return Ok(dtos); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting models using capabilities {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while retrieving models"); - } - } - - /// - /// Creates a new model capabilities - /// - /// The model capabilities to create - /// The created model capabilities - [HttpPost] - [ProducesResponseType(typeof(CapabilitiesDto), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task Create([FromBody] CreateCapabilitiesDto dto) - { - try - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - var capabilities = new ModelCapabilities - { - 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, - SupportsChat = dto.SupportsChat, - SupportsFunctionCalling = dto.SupportsFunctionCalling, - SupportsStreaming = dto.SupportsStreaming, - TokenizerType = dto.TokenizerType, - SupportedVoices = dto.SupportedVoices, - SupportedLanguages = dto.SupportedLanguages, - SupportedFormats = dto.SupportedFormats - }; - - await _repository.CreateAsync(capabilities); - - return CreatedAtAction( - nameof(GetById), - new { id = capabilities.Id }, - MapToDto(capabilities)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating model capabilities"); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while creating the model capabilities"); - } - } - - /// - /// Updates an existing model capabilities - /// - /// The capabilities ID - /// The updated model capabilities data - /// No content on success - [HttpPut("{id}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task Update(int id, [FromBody] UpdateCapabilitiesDto dto) - { - try - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - if (id != dto.Id) - { - return BadRequest("ID mismatch"); - } - - var capabilities = await _repository.GetByIdAsync(id); - if (capabilities == null) - { - return NotFound($"Model capabilities with ID {id} not found"); - } - - // Update fields - if (dto.MaxTokens.HasValue) - capabilities.MaxTokens = dto.MaxTokens.Value; - if (dto.MinTokens.HasValue) - 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) - capabilities.SupportsVideoGeneration = dto.SupportsVideoGeneration.Value; - if (dto.SupportsEmbeddings.HasValue) - capabilities.SupportsEmbeddings = dto.SupportsEmbeddings.Value; - if (dto.SupportsChat.HasValue) - capabilities.SupportsChat = dto.SupportsChat.Value; - if (dto.SupportsFunctionCalling.HasValue) - capabilities.SupportsFunctionCalling = dto.SupportsFunctionCalling.Value; - if (dto.SupportsStreaming.HasValue) - 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); - - return NoContent(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating model capabilities with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while updating the model capabilities"); - } - } - - /// - /// Deletes a model capabilities - /// - /// The capabilities ID - /// No content on success - [HttpDelete("{id}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task Delete(int id) - { - try - { - var capabilities = await _repository.GetByIdAsync(id); - if (capabilities == null) - { - return NotFound($"Model capabilities with ID {id} not found"); - } - - // Check if capabilities is used by models - var models = await _repository.GetModelsUsingCapabilitiesAsync(id); - if (models != null && models.Any()) - { - return Conflict($"Cannot delete model capabilities used by {models.Count()} models. Update the models first."); - } - - await _repository.DeleteAsync(id); - - return NoContent(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting model capabilities with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while deleting the model capabilities"); - } - } - - private static CapabilitiesDto MapToDto(ModelCapabilities capabilities) - { - return new CapabilitiesDto - { - Id = capabilities.Id, - 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, - SupportsChat = capabilities.SupportsChat, - 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 deleted file mode 100644 index 3f54195f4..000000000 --- a/ConduitLLM.Admin/Controllers/ModelController.cs +++ /dev/null @@ -1,421 +0,0 @@ -using ConduitLLM.Admin.Models.Models; -using ConduitLLM.Admin.Models.ModelCapabilities; -using ConduitLLM.Admin.Models.ModelSeries; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.Repositories; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace ConduitLLM.Admin.Controllers -{ - /// - /// Controller for managing canonical Model entities - /// - [ApiController] - [Route("api/[controller]")] - [Authorize(Policy = "MasterKeyPolicy")] - public class ModelController : ControllerBase - { - private readonly IModelRepository _modelRepository; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the ModelController - /// - public ModelController( - IModelRepository modelRepository, - ILogger logger) - { - _modelRepository = modelRepository ?? throw new ArgumentNullException(nameof(modelRepository)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Gets all models with their capabilities - /// - /// List of all models - [HttpGet] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetAllModels() - { - try - { - var models = await _modelRepository.GetAllWithDetailsAsync(); - var dtos = models.Select(m => MapToDto(m)); - return Ok(dtos); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting all models"); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while retrieving models"); - } - } - - /// - /// Gets a specific model by ID - /// - /// The model ID - /// The model with its capabilities - [HttpGet("{id}")] - [ProducesResponseType(typeof(ModelDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetModelById(int id) - { - try - { - var model = await _modelRepository.GetByIdWithDetailsAsync(id); - if (model == null) - { - return NotFound($"Model with ID {id} not found"); - } - - return Ok(MapToDto(model)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting model with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while retrieving the model"); - } - } - - - /// - /// Searches for models by name - /// - /// The search query - /// List of matching models - [HttpGet("search")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task SearchModels([FromQuery] string query) - { - try - { - if (string.IsNullOrWhiteSpace(query)) - { - return Ok(new List()); - } - - var models = await _modelRepository.SearchByNameAsync(query); - var dtos = models.Select(m => MapToDto(m)); - return Ok(dtos); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error searching models with query {Query}", query); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while searching models"); - } - } - - /// - /// Gets models available from a specific provider - /// - /// The provider name (e.g., "groq", "openai", "anthropic") - /// List of models available from the provider - [HttpGet("provider/{provider}")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetModelsByProvider(string provider) - { - try - { - if (string.IsNullOrWhiteSpace(provider)) - { - return BadRequest("Provider name is required"); - } - - var models = await _modelRepository.GetByProviderAsync(provider); - var dtos = models.Select(m => - { - // Find the identifier for this specific provider - var providerIdentifier = m.Identifiers?.FirstOrDefault(i => - string.Equals(i.Provider, provider, StringComparison.OrdinalIgnoreCase))?.Identifier - ?? m.Name; // Fallback to model name if no specific identifier - - return new ModelWithProviderIdDto - { - Id = m.Id, - Name = m.Name, - ProviderModelId = providerIdentifier, - ModelSeriesId = m.ModelSeriesId, - ModelCapabilitiesId = m.ModelCapabilitiesId, - Capabilities = m.Capabilities != null ? MapCapabilitiesToDto(m.Capabilities) : null, - IsActive = m.IsActive, - CreatedAt = m.CreatedAt, - UpdatedAt = m.UpdatedAt - }; - }); - return Ok(dtos); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting models for provider {Provider}", provider); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while retrieving models"); - } - } - - /// - /// Gets model identifiers for a specific model - /// - /// The model ID - /// List of model identifiers showing which providers offer this model - [HttpGet("{id}/identifiers")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetModelIdentifiers(int id) - { - try - { - var model = await _modelRepository.GetByIdWithDetailsAsync(id); - if (model == null) - { - return NotFound($"Model with ID {id} not found"); - } - - var identifiers = model.Identifiers.Select(i => new - { - id = i.Id, - identifier = i.Identifier, - provider = i.Provider, - isPrimary = i.IsPrimary - }); - - return Ok(identifiers); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting identifiers for model with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while retrieving model identifiers"); - } - } - - /// - /// Creates a new model - /// - /// The model to create - /// The created model - [HttpPost] - [ProducesResponseType(typeof(ModelDto), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task CreateModel([FromBody] CreateModelDto dto) - { - try - { - if (dto == null) - { - return BadRequest("Model data is required"); - } - - if (string.IsNullOrWhiteSpace(dto.Name)) - { - return BadRequest("Model name is required"); - } - - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - // Check if a model with the same name already exists - var existing = await _modelRepository.GetByNameAsync(dto.Name); - if (existing != null) - { - return Conflict($"A model with name '{dto.Name}' already exists"); - } - - var model = new Model - { - Name = dto.Name, - ModelSeriesId = dto.ModelSeriesId, - ModelCapabilitiesId = dto.ModelCapabilitiesId, - ModelParameters = dto.ModelParameters, - IsActive = dto.IsActive ?? true, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }; - - await _modelRepository.CreateAsync(model); - - // Reload with capabilities - model = await _modelRepository.GetByIdWithDetailsAsync(model.Id); - if (model == null) - { - return StatusCode(StatusCodes.Status500InternalServerError, "Failed to reload created model"); - } - - return CreatedAtAction( - nameof(GetModelById), - new { id = model.Id }, - MapToDto(model)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating model"); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while creating the model"); - } - } - - /// - /// Updates an existing model - /// - /// The model ID - /// The updated model data - /// No content on success - [HttpPut("{id}")] - [ProducesResponseType(typeof(ModelDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task UpdateModel(int id, [FromBody] UpdateModelDto dto) - { - try - { - if (dto == null) - { - return BadRequest("Update data is required"); - } - - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - var model = await _modelRepository.GetByIdWithDetailsAsync(id); - if (model == null) - { - return NotFound($"Model with ID {id} not found"); - } - - // Check for name conflicts if name is being changed - if (!string.IsNullOrEmpty(dto.Name) && dto.Name != model.Name) - { - var existing = await _modelRepository.GetByNameAsync(dto.Name); - if (existing != null && existing.Id != id) - { - return Conflict($"A model with name '{dto.Name}' already exists"); - } - model.Name = dto.Name; - } - - if (dto.ModelSeriesId.HasValue) - model.ModelSeriesId = dto.ModelSeriesId.Value; - if (dto.ModelCapabilitiesId.HasValue) - model.ModelCapabilitiesId = dto.ModelCapabilitiesId.Value; - if (dto.IsActive.HasValue) - model.IsActive = dto.IsActive.Value; - if (dto.ModelParameters != null) - model.ModelParameters = string.IsNullOrWhiteSpace(dto.ModelParameters) ? null : dto.ModelParameters; - - model.UpdatedAt = DateTime.UtcNow; - - var updatedModel = await _modelRepository.UpdateAsync(model); - - return Ok(MapToDto(updatedModel)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating model with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while updating the model"); - } - } - - /// - /// Deletes a model - /// - /// The model ID - /// No content on success - [HttpDelete("{id}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task DeleteModel(int id) - { - try - { - var model = await _modelRepository.GetByIdAsync(id); - if (model == null) - { - return NotFound($"Model with ID {id} not found"); - } - - // Check if model is referenced by any mappings - var hasReferences = await _modelRepository.HasMappingReferencesAsync(id); - if (hasReferences) - { - return Conflict("Cannot delete model that is referenced by model provider mappings"); - } - - await _modelRepository.DeleteAsync(id); - - return NoContent(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting model with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while deleting the model"); - } - } - - private static ModelDto MapToDto(Model model) - { - return new ModelDto - { - Id = model.Id, - Name = model.Name, - ModelSeriesId = model.ModelSeriesId, - ModelCapabilitiesId = model.ModelCapabilitiesId, - Capabilities = model.Capabilities != null ? MapCapabilitiesToDto(model.Capabilities) : null, - IsActive = model.IsActive, - CreatedAt = model.CreatedAt, - UpdatedAt = model.UpdatedAt, - Series = model.Series != null ? MapSeriesToDto(model.Series) : null, - ModelParameters = model.ModelParameters - }; - } - - private static ModelSeriesDto MapSeriesToDto(ModelSeries series) - { - return new ModelSeriesDto - { - Id = series.Id, - AuthorId = series.AuthorId, - AuthorName = series.Author?.Name, - Name = series.Name, - Description = series.Description, - TokenizerType = series.TokenizerType, - Parameters = series.Parameters - }; - } - - private static ModelCapabilitiesDto MapCapabilitiesToDto(ModelCapabilities capabilities) - { - return new ModelCapabilitiesDto - { - Id = capabilities.Id, - SupportsChat = capabilities.SupportsChat, - 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 - }; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Admin/Controllers/ModelProviderMappingController.cs b/ConduitLLM.Admin/Controllers/ModelProviderMappingController.cs deleted file mode 100644 index 6ab088a7c..000000000 --- a/ConduitLLM.Admin/Controllers/ModelProviderMappingController.cs +++ /dev/null @@ -1,315 +0,0 @@ -using ConduitLLM.Configuration.Interfaces; - -using ConduitLLM.Admin.Interfaces; -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.Extensions; - -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace ConduitLLM.Admin.Controllers; - -/// -/// Controller for managing model provider mappings -/// -[ApiController] -[Route("api/[controller]")] -[Authorize(Policy = "MasterKeyPolicy")] -public class ModelProviderMappingController : ControllerBase -{ - private readonly IAdminModelProviderMappingService _mappingService; - private readonly IProviderService _providerService; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the ModelProviderMappingController - /// - /// The model provider mapping service - /// The provider service - /// The logger - public ModelProviderMappingController( - IAdminModelProviderMappingService mappingService, - IProviderService providerService, - ILogger logger) - { - _mappingService = mappingService ?? throw new ArgumentNullException(nameof(mappingService)); - _providerService = providerService ?? throw new ArgumentNullException(nameof(providerService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Gets all model provider mappings - /// - /// A list of all model provider mappings - [HttpGet] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetAllMappings() - { - try - { - var mappings = await _mappingService.GetAllMappingsAsync(); - var dtos = mappings.Select(m => m.ToDto()); - return Ok(dtos); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting all model provider mappings"); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while retrieving model provider mappings"); - } - } - - /// - /// Gets a specific model provider mapping by ID - /// - /// The ID of the mapping to retrieve - /// The model provider mapping - [HttpGet("{id}")] - [ProducesResponseType(typeof(ModelProviderMappingDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetMappingById(int id) - { - try - { - var mapping = await _mappingService.GetMappingByIdAsync(id); - - if (mapping == null) - { - return NotFound(new ErrorResponseDto("Model provider mapping not found")); - } - - return Ok(mapping.ToDto()); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting model provider mapping with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while retrieving the model provider mapping"); - } - } - - /// - /// Creates a new model provider mapping - /// - /// The mapping to create - /// The created mapping - [HttpPost] - [ProducesResponseType(typeof(ModelProviderMappingDto), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task CreateMapping([FromBody] ModelProviderMappingDto mappingDto) - { - try - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - // Check if a mapping with the same model ID already exists - var existingMapping = await _mappingService.GetMappingByModelIdAsync(mappingDto.ModelId); - if (existingMapping != null) - { - return Conflict(new ErrorResponseDto($"A mapping for model ID '{mappingDto.ModelId}' already exists")); - } - - var mapping = mappingDto.ToEntity(); - var success = await _mappingService.AddMappingAsync(mapping); - - if (!success) - { - return BadRequest(new ErrorResponseDto("Failed to create model provider mapping. Please check the provider ID.")); - } - - var createdMapping = await _mappingService.GetMappingByModelIdAsync(mappingDto.ModelId); - return CreatedAtAction(nameof(GetMappingById), new { id = createdMapping?.Id }, createdMapping?.ToDto()); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating model provider mapping"); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while creating the model provider mapping"); - } - } - - /// - /// Updates an existing model provider mapping - /// - /// The ID of the mapping to update - /// The updated mapping data - /// No content on success - [HttpPut("{id}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task UpdateMapping(int id, [FromBody] ModelProviderMappingDto mappingDto) - { - try - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - if (id != mappingDto.Id) - { - return BadRequest(new ErrorResponseDto("ID mismatch")); - } - - var existingMapping = await _mappingService.GetMappingByIdAsync(id); - if (existingMapping == null) - { - return NotFound(new ErrorResponseDto("Model provider mapping not found")); - } - - existingMapping.UpdateFromDto(mappingDto); - var success = await _mappingService.UpdateMappingAsync(existingMapping); - - if (!success) - { - return BadRequest(new ErrorResponseDto("Failed to update model provider mapping")); - } - - return NoContent(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating model provider mapping with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while updating the model provider mapping"); - } - } - - /// - /// Deletes a model provider mapping - /// - /// The ID of the mapping to delete - /// No content on success - [HttpDelete("{id}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task DeleteMapping(int id) - { - try - { - var existingMapping = await _mappingService.GetMappingByIdAsync(id); - if (existingMapping == null) - { - return NotFound(new ErrorResponseDto("Model provider mapping not found")); - } - - var success = await _mappingService.DeleteMappingAsync(id); - - if (!success) - { - return BadRequest(new ErrorResponseDto("Failed to delete model provider mapping")); - } - - return NoContent(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting model provider mapping with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while deleting the model provider mapping"); - } - } - - /// - /// Gets all available providers - /// - /// List of providers with IDs and names - [HttpGet("providers")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetProviders() - { - try - { - var providers = await _mappingService.GetProvidersAsync(); - return Ok(providers); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting providers"); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while retrieving providers"); - } - } - - /// - /// Creates multiple model provider mappings in a single operation - /// - /// The mappings to create - /// The bulk mapping response with results - [HttpPost("bulk")] - [ProducesResponseType(typeof(BulkMappingResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task CreateBulkMappings([FromBody] List mappingDtos) - { - try - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - if (mappingDtos == null || mappingDtos.Count() == 0) - { - return BadRequest(new ErrorResponseDto("No mappings provided")); - } - - var mappings = mappingDtos.Select(dto => dto.ToEntity()).ToList(); - var (created, errors) = await _mappingService.CreateBulkMappingsAsync(mappings); - - var result = new BulkMappingResult - { - Created = created.Select(m => m.ToDto()).ToList(), - Errors = errors.ToList(), - TotalProcessed = mappingDtos.Count(), - SuccessCount = created.Count(), - FailureCount = errors.Count() - }; - - return Ok(result); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating bulk model provider mappings"); - return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while creating bulk model provider mappings"); - } - } - -} - -/// -/// Result of a bulk mapping operation -/// -public class BulkMappingResult -{ - /// - /// Successfully created mappings - /// - public List Created { get; set; } = new(); - - /// - /// Error messages for failed mappings - /// - public List Errors { get; set; } = new(); - - /// - /// Total number of mappings processed - /// - public int TotalProcessed { get; set; } - - /// - /// Number of successful mappings - /// - public int SuccessCount { get; set; } - - /// - /// Number of failed mappings - /// - public int FailureCount { get; set; } -} diff --git a/ConduitLLM.Admin/Controllers/ProviderCredentialsController.Testing.cs b/ConduitLLM.Admin/Controllers/ProviderCredentialsController.Testing.cs deleted file mode 100644 index 13dba0e30..000000000 --- a/ConduitLLM.Admin/Controllers/ProviderCredentialsController.Testing.cs +++ /dev/null @@ -1,219 +0,0 @@ -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.DTOs; -using Microsoft.AspNetCore.Mvc; - -namespace ConduitLLM.Admin.Controllers -{ - public partial class ProviderCredentialsController - { - /// - /// Tests the connection to a provider - /// - /// The ID of the provider to test - /// The test result - [HttpPost("test/{id}")] - [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task TestProviderConnection(int id) - { - try - { - var provider = await _providerRepository.GetByIdAsync(id); - if (provider == null) - { - return NotFound(new ErrorResponseDto("Provider not found")); - } - - // Get a client for this provider to test - var client = _clientFactory.GetClientByProviderId(id); - - // Perform a simple test - list models - try - { - var models = await client.ListModelsAsync(); - return Ok(new - { - Success = true, - ProviderId = id, - ProviderType = provider.ProviderType.ToString(), - ProviderName = provider.ProviderName, - ModelCount = models?.Count() ?? 0, - ResponseTime = DateTime.UtcNow, - Message = "Connection successful" - }); - } - catch (Exception testEx) - { - return Ok(new - { - Success = false, - ProviderId = id, - ProviderType = provider.ProviderType.ToString(), - ProviderName = provider.ProviderName, - ModelCount = 0, - ResponseTime = DateTime.UtcNow, - Message = testEx.Message - }); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error testing connection for provider with ID {Id}", id); - return StatusCode(StatusCodes.Status500InternalServerError, "An unexpected error occurred."); - } - } - - /// - /// Tests a provider connection without saving - /// - /// The test result - [HttpPost("test")] - [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task TestProviderConnectionWithCredentials([FromBody] TestProviderRequest testRequest) - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - try - { - // Create a temporary provider for testing - var testProvider = new Provider - { - Id = -1, // Temporary ID - ProviderType = testRequest.ProviderType, - ProviderName = "Test Provider", - BaseUrl = testRequest.BaseUrl, - IsEnabled = true - }; - - // Create a temporary key if provided - if (!string.IsNullOrEmpty(testRequest.ApiKey)) - { - testProvider.ProviderKeyCredentials = new List - { - new ProviderKeyCredential - { - ApiKey = testRequest.ApiKey, - Organization = testRequest.Organization, - IsPrimary = true, - IsEnabled = true - } - }; - } - - // Test the connection - var testKey = new ProviderKeyCredential - { - ApiKey = testRequest.ApiKey, - BaseUrl = testRequest.BaseUrl, - Organization = testRequest.Organization, - IsPrimary = true, - IsEnabled = true - }; - var client = _clientFactory.CreateTestClient(testProvider, testKey); - - try - { - var models = await client.ListModelsAsync(); - return Ok(new - { - Success = true, - ProviderType = testProvider.ProviderType.ToString(), - ModelCount = models?.Count() ?? 0, - ResponseTime = DateTime.UtcNow, - Message = "Connection successful" - }); - } - catch (Exception testEx) - { - return Ok(new - { - Success = false, - ProviderType = testProvider.ProviderType.ToString(), - ModelCount = 0, - ResponseTime = DateTime.UtcNow, - Message = testEx.Message - }); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error testing connection for provider {ProviderType}", testRequest?.ProviderType.ToString() ?? "unknown"); - return StatusCode(StatusCodes.Status500InternalServerError, "An unexpected error occurred."); - } - } - - /// - /// Tests a specific key credential - /// - /// The ID of the provider - /// The ID of the key to test - /// The test result - [HttpPost("{providerId}/keys/{keyId}/test")] - [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task TestProviderKeyCredential(int providerId, int keyId) - { - try - { - var key = await _keyRepository.GetByIdAsync(keyId); - if (key == null || key.ProviderId != providerId) - { - return NotFound(new ErrorResponseDto("Key credential not found")); - } - - var provider = await _providerRepository.GetByIdAsync(providerId); - if (provider == null) - { - return NotFound(new ErrorResponseDto("Provider not found")); - } - - // Test the connection with this specific key - var client = _clientFactory.CreateTestClient(provider, key); - - try - { - var models = await client.ListModelsAsync(); - return Ok(new - { - Success = true, - ProviderId = providerId, - KeyId = keyId, - KeyName = key.KeyName, - ProviderType = provider.ProviderType.ToString(), - ProviderName = provider.ProviderName, - ModelCount = models?.Count() ?? 0, - ResponseTime = DateTime.UtcNow, - Message = "Connection successful" - }); - } - catch (Exception testEx) - { - return Ok(new - { - Success = false, - ProviderId = providerId, - KeyId = keyId, - KeyName = key.KeyName, - ProviderType = provider.ProviderType.ToString(), - ProviderName = provider.ProviderName, - ModelCount = 0, - ResponseTime = DateTime.UtcNow, - Message = testEx.Message - }); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error testing key credential {KeyId} for provider {ProviderId}", keyId, providerId); - return StatusCode(StatusCodes.Status500InternalServerError, "An unexpected error occurred."); - } - } - } -} 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/Controllers/SystemInfoController.cs b/ConduitLLM.Admin/Controllers/SystemInfoController.cs deleted file mode 100644 index 724c43109..000000000 --- a/ConduitLLM.Admin/Controllers/SystemInfoController.cs +++ /dev/null @@ -1,74 +0,0 @@ -using ConduitLLM.Admin.Interfaces; -using ConduitLLM.Configuration.DTOs.Monitoring; - -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace ConduitLLM.Admin.Controllers; - -/// -/// Controller for system information -/// -[ApiController] -[Route("api/[controller]")] -[Authorize(Policy = "MasterKeyPolicy")] -public class SystemInfoController : ControllerBase -{ - private readonly IAdminSystemInfoService _systemInfoService; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the SystemInfoController - /// - /// The system info service - /// The logger - public SystemInfoController( - IAdminSystemInfoService systemInfoService, - ILogger logger) - { - _systemInfoService = systemInfoService ?? throw new ArgumentNullException(nameof(systemInfoService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Gets system information - /// - /// System information details - [HttpGet("info")] - [ProducesResponseType(typeof(SystemInfoDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetSystemInfo() - { - try - { - var systemInfo = await _systemInfoService.GetSystemInfoAsync(); - return Ok(systemInfo); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting system information"); - return StatusCode(StatusCodes.Status500InternalServerError, "An unexpected error occurred."); - } - } - - /// - /// Gets health status - /// - /// Health status information - [HttpGet("health")] - [ProducesResponseType(typeof(HealthStatusDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetHealthStatus() - { - try - { - var healthStatus = await _systemInfoService.GetHealthStatusAsync(); - return Ok(healthStatus); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting health status"); - return StatusCode(StatusCodes.Status500InternalServerError, "An unexpected error occurred."); - } - } -} diff --git a/ConduitLLM.Admin/Controllers/VirtualKeyGroupsController.cs b/ConduitLLM.Admin/Controllers/VirtualKeyGroupsController.cs deleted file mode 100644 index 3136a12e8..000000000 --- a/ConduitLLM.Admin/Controllers/VirtualKeyGroupsController.cs +++ /dev/null @@ -1,384 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.Interfaces; -using ConduitLLM.Configuration.DTOs.VirtualKey; - -namespace ConduitLLM.Admin.Controllers -{ - /// - /// Controller for managing virtual key groups - /// - [Authorize] - [ApiController] - [Route("api/[controller]")] - public class VirtualKeyGroupsController : ControllerBase - { - private readonly IVirtualKeyGroupRepository _groupRepository; - private readonly IVirtualKeyRepository _keyRepository; - private readonly IConfigurationDbContext _context; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the VirtualKeyGroupsController - /// - public VirtualKeyGroupsController( - IVirtualKeyGroupRepository groupRepository, - IVirtualKeyRepository keyRepository, - IConfigurationDbContext context, - ILogger logger) - { - _groupRepository = groupRepository; - _keyRepository = keyRepository; - _context = context; - _logger = logger; - } - - /// - /// Get all virtual key groups - /// - [HttpGet] - public async Task>> GetAllGroups() - { - try - { - _logger.LogInformation("GetAllGroups called"); - var groups = await _groupRepository.GetAllAsync(); - _logger.LogInformation("Repository returned {Count} groups", groups.Count()); - var dtos = groups.Select(g => - { - _logger.LogInformation("Group {GroupId} has {KeyCount} keys (null: {IsNull})", - g.Id, g.VirtualKeys?.Count ?? -1, g.VirtualKeys == null); - - return new VirtualKeyGroupDto - { - Id = g.Id, - ExternalGroupId = g.ExternalGroupId, - GroupName = g.GroupName, - Balance = g.Balance, - LifetimeCreditsAdded = g.LifetimeCreditsAdded, - LifetimeSpent = g.LifetimeSpent, - CreatedAt = g.CreatedAt, - UpdatedAt = g.UpdatedAt, - VirtualKeyCount = g.VirtualKeys?.Count ?? 0 - }; - }).ToList(); - - return Ok(dtos); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving virtual key groups"); - return StatusCode(500, new { message = "An error occurred while retrieving groups" }); - } - } - - /// - /// Get a specific virtual key group by ID - /// - [HttpGet("{id}")] - public async Task> GetGroup(int id) - { - try - { - var group = await _groupRepository.GetByIdWithKeysAsync(id); - if (group == null) - { - return NotFound(new { message = "Group not found" }); - } - - var dto = new VirtualKeyGroupDto - { - Id = group.Id, - ExternalGroupId = group.ExternalGroupId, - GroupName = group.GroupName, - Balance = group.Balance, - LifetimeCreditsAdded = group.LifetimeCreditsAdded, - LifetimeSpent = group.LifetimeSpent, - CreatedAt = group.CreatedAt, - UpdatedAt = group.UpdatedAt, - VirtualKeyCount = group.VirtualKeys?.Count ?? 0 - }; - - return Ok(dto); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving virtual key group {GroupId}", id); - return StatusCode(500, new { message = "An error occurred while retrieving the group" }); - } - } - - /// - /// Create a new virtual key group - /// - [HttpPost] - public async Task> CreateGroup([FromBody] CreateVirtualKeyGroupRequestDto request) - { - try - { - var group = new VirtualKeyGroup - { - ExternalGroupId = request.ExternalGroupId, - GroupName = request.GroupName, - Balance = request.InitialBalance ?? 0, - LifetimeCreditsAdded = request.InitialBalance ?? 0, - LifetimeSpent = 0 - }; - - var id = await _groupRepository.CreateAsync(group); - group.Id = id; - - var dto = new VirtualKeyGroupDto - { - Id = group.Id, - ExternalGroupId = group.ExternalGroupId, - GroupName = group.GroupName, - Balance = group.Balance, - LifetimeCreditsAdded = group.LifetimeCreditsAdded, - LifetimeSpent = group.LifetimeSpent, - CreatedAt = group.CreatedAt, - UpdatedAt = group.UpdatedAt, - VirtualKeyCount = 0 - }; - - return CreatedAtAction(nameof(GetGroup), new { id = group.Id }, dto); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating virtual key group"); - return StatusCode(500, new { message = "An error occurred while creating the group" }); - } - } - - /// - /// Update a virtual key group - /// - [HttpPut("{id}")] - public async Task UpdateGroup(int id, [FromBody] UpdateVirtualKeyGroupRequestDto request) - { - try - { - var group = await _groupRepository.GetByIdAsync(id); - if (group == null) - { - return NotFound(new { message = "Group not found" }); - } - - if (!string.IsNullOrEmpty(request.GroupName)) - { - group.GroupName = request.GroupName; - } - - if (!string.IsNullOrEmpty(request.ExternalGroupId)) - { - group.ExternalGroupId = request.ExternalGroupId; - } - - await _groupRepository.UpdateAsync(group); - return NoContent(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating virtual key group {GroupId}", id); - return StatusCode(500, new { message = "An error occurred while updating the group" }); - } - } - - /// - /// Adjust the balance of a virtual key group - /// - [HttpPost("{id}/adjust-balance")] - public async Task> AdjustBalance(int id, [FromBody] AdjustBalanceDto request) - { - try - { - // Get the authenticated user's identity - var initiatedBy = User.Identity?.Name ?? "System"; - - var newBalance = await _groupRepository.AdjustBalanceAsync( - id, - request.Amount, - request.Description, - initiatedBy - ); - - var group = await _groupRepository.GetByIdAsync(id); - if (group == null) - { - return NotFound(new { message = "Group not found" }); - } - - var dto = new VirtualKeyGroupDto - { - Id = group.Id, - ExternalGroupId = group.ExternalGroupId, - GroupName = group.GroupName, - Balance = group.Balance, - LifetimeCreditsAdded = group.LifetimeCreditsAdded, - LifetimeSpent = group.LifetimeSpent, - CreatedAt = group.CreatedAt, - UpdatedAt = group.UpdatedAt, - VirtualKeyCount = group.VirtualKeys?.Count ?? 0 - }; - - return Ok(dto); - } - catch (InvalidOperationException ex) - { - return BadRequest(new { message = ex.Message }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error adjusting balance for virtual key group {GroupId}", id); - return StatusCode(500, new { message = "An error occurred while adjusting the balance" }); - } - } - - /// - /// Delete a virtual key group - /// - [HttpDelete("{id}")] - public async Task DeleteGroup(int id) - { - try - { - var group = await _groupRepository.GetByIdAsync(id); - if (group == null) - { - return NotFound(new { message = "Group not found" }); - } - - // Check if group has any keys - if (group.VirtualKeys?.Count > 0) - { - return BadRequest(new { message = "Cannot delete group with existing virtual keys" }); - } - - await _groupRepository.DeleteAsync(id); - return NoContent(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting virtual key group {GroupId}", id); - return StatusCode(500, new { message = "An error occurred while deleting the group" }); - } - } - - /// - /// Get transaction history for a virtual key group - /// - [HttpGet("{id}/transactions")] - [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetTransactionHistory( - int id, - [FromQuery] int page = 1, - [FromQuery] int pageSize = 50) - { - try - { - var group = await _groupRepository.GetByIdAsync(id); - if (group == null) - { - return NotFound(new { message = "Group not found" }); - } - - // Validate page parameters - if (page < 1) page = 1; - if (pageSize < 1) pageSize = 50; - if (pageSize > 100) pageSize = 100; - - // Get total count - var totalCount = await _context.VirtualKeyGroupTransactions - .Where(t => t.VirtualKeyGroupId == id && !t.IsDeleted) - .CountAsync(); - - // Calculate pagination - var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); - var skip = (page - 1) * pageSize; - - // Get paginated transactions - var transactions = await _context.VirtualKeyGroupTransactions - .Where(t => t.VirtualKeyGroupId == id && !t.IsDeleted) - .OrderByDescending(t => t.CreatedAt) - .Skip(skip) - .Take(pageSize) - .Select(t => new VirtualKeyGroupTransactionDto - { - Id = t.Id, - VirtualKeyGroupId = t.VirtualKeyGroupId, - TransactionType = t.TransactionType, - Amount = t.Amount, - BalanceAfter = t.BalanceAfter, - Description = t.Description, - ReferenceId = t.ReferenceId, - ReferenceType = t.ReferenceType, - InitiatedBy = t.InitiatedBy, - InitiatedByUserId = t.InitiatedByUserId, - CreatedAt = t.CreatedAt - }) - .ToListAsync(); - - var result = new PagedResult - { - Items = transactions, - TotalCount = totalCount, - CurrentPage = page, - PageSize = pageSize, - TotalPages = totalPages - }; - - return Ok(result); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving transaction history for virtual key group {GroupId}", id); - return StatusCode(500, new { message = "An error occurred while retrieving the transaction history" }); - } - } - - /// - /// Get virtual keys in a group - /// - [HttpGet("{id}/keys")] - public async Task>> GetKeysInGroup(int id) - { - try - { - var group = await _groupRepository.GetByIdWithKeysAsync(id); - if (group == null) - { - return NotFound(new { message = "Group not found" }); - } - - var keys = group.VirtualKeys?.Select(k => new VirtualKeyDto - { - Id = k.Id, - KeyName = k.KeyName, - KeyPrefix = k.KeyHash?.Length > 10 ? k.KeyHash.Substring(0, 10) + "..." : k.KeyHash, - AllowedModels = k.AllowedModels, - VirtualKeyGroupId = k.VirtualKeyGroupId, - IsEnabled = k.IsEnabled, - ExpiresAt = k.ExpiresAt, - CreatedAt = k.CreatedAt, - UpdatedAt = k.UpdatedAt, - Metadata = k.Metadata, - RateLimitRpm = k.RateLimitRpm, - RateLimitRpd = k.RateLimitRpd, - Description = k.Description - }).ToList() ?? new List(); - - return Ok(keys); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving keys for virtual key group {GroupId}", id); - return StatusCode(500, new { message = "An error occurred while retrieving the keys" }); - } - } - } -} diff --git a/ConduitLLM.Admin/Dockerfile b/ConduitLLM.Admin/Dockerfile deleted file mode 100644 index 2eea1c44c..000000000 --- a/ConduitLLM.Admin/Dockerfile +++ /dev/null @@ -1,63 +0,0 @@ -# Admin API Dockerfile - Optimized for selective cache invalidation -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build -WORKDIR /src - -# ===== CACHED LAYERS (Keep these fast) ===== -# Copy project files for dependency resolution -COPY *.sln . -COPY Directory.Build.props ./ -COPY */*.csproj ./ -RUN find . -name "*.csproj" -exec dirname {} \; | xargs -I {} mkdir -p {} - -# Copy project files to correct locations -COPY ConduitLLM.Http/*.csproj ./ConduitLLM.Http/ -COPY ConduitLLM.Configuration/*.csproj ./ConduitLLM.Configuration/ -COPY ConduitLLM.Core/*.csproj ./ConduitLLM.Core/ -COPY ConduitLLM.Security/*.csproj ./ConduitLLM.Security/ -COPY ConduitLLM.Providers/*.csproj ./ConduitLLM.Providers/ -COPY ConduitLLM.Admin/*.csproj ./ConduitLLM.Admin/ -COPY ConduitLLM.Tests/*.csproj ./ConduitLLM.Tests/ -COPY ConduitLLM.IntegrationTests/*.csproj ./ConduitLLM.IntegrationTests/ - -# Restore dependencies (cached unless project files change) -RUN dotnet restore "ConduitLLM.Admin/ConduitLLM.Admin.csproj" - -# ===== CACHE INVALIDATION POINT ===== -# This argument forces rebuild from here down when .NET code changes -ARG CACHEBUST=1 - -# Copy all source code (this and everything after will rebuild) -COPY . . - -# Build and publish -RUN dotnet publish "ConduitLLM.Admin/ConduitLLM.Admin.csproj" \ - -c Release \ - -o /app/publish \ - --no-restore - -# Runtime stage - Debian-based for reliability -FROM mcr.microsoft.com/dotnet/aspnet:9.0 -WORKDIR /app - -# Install curl for health checks -RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* - -# Copy published application -COPY --from=build /app/publish . - -# Create non-root user -RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app -USER appuser - -# Configure ASP.NET Core -ENV ASPNETCORE_URLS=http://+:8080 -ENV ASPNETCORE_ENVIRONMENT=Production -ENV DOTNET_RUNNING_IN_CONTAINER=true - -EXPOSE 8080 - -# Health check -HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=3 \ - CMD curl -f http://localhost:8080/health/ready || exit 1 - -ENTRYPOINT ["dotnet", "ConduitLLM.Admin.dll"] \ No newline at end of file diff --git a/ConduitLLM.Admin/Extensions/ConfigurationExtensions.cs b/ConduitLLM.Admin/Extensions/ConfigurationExtensions.cs deleted file mode 100644 index 17cd3feba..000000000 --- a/ConduitLLM.Admin/Extensions/ConfigurationExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Extensions; -using ConduitLLM.Configuration.Interfaces; - -namespace ConduitLLM.Admin.Extensions -{ - /// - /// Extension methods for configuring Configuration services in the Admin API - /// - public static class ConfigurationExtensions - { - /// - /// Adds the Configuration services to the DI container - /// - /// The service collection - /// The application configuration - /// The service collection for chaining - public static IServiceCollection AddConfigurationServices(this IServiceCollection services, IConfiguration configuration) - { - // Add repositories - services.AddRepositories(); - - // Add caching services - services.AddCachingServices(configuration); - - // Add database initialization - services.AddDatabaseInitialization(); - - // Add Configuration services - services.AddScoped(); - services.AddScoped(); - - return services; - } - } -} 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/SecurityOptionsExtensions.cs b/ConduitLLM.Admin/Extensions/SecurityOptionsExtensions.cs deleted file mode 100644 index e579ce4c5..000000000 --- a/ConduitLLM.Admin/Extensions/SecurityOptionsExtensions.cs +++ /dev/null @@ -1,84 +0,0 @@ -using ConduitLLM.Admin.Options; - -namespace ConduitLLM.Admin.Extensions -{ - /// - /// Extension methods for configuring security options - /// - public static class SecurityOptionsExtensions - { - /// - /// Configures security options from environment variables - /// - public static IServiceCollection ConfigureAdminSecurityOptions(this IServiceCollection services, IConfiguration configuration) - { - services.Configure(options => - { - // IP Filtering - options.IpFiltering.Enabled = configuration.GetValue("CONDUIT_ADMIN_IP_FILTERING_ENABLED", false); - options.IpFiltering.Mode = configuration["CONDUIT_ADMIN_IP_FILTER_MODE"] ?? "permissive"; - options.IpFiltering.AllowPrivateIps = configuration.GetValue("CONDUIT_ADMIN_IP_FILTER_ALLOW_PRIVATE", true); - - // Parse whitelist and blacklist from comma-separated values - var whitelist = configuration["CONDUIT_ADMIN_IP_FILTER_WHITELIST"]; - if (!string.IsNullOrWhiteSpace(whitelist)) - { - options.IpFiltering.Whitelist = whitelist.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()).ToList(); - } - - var blacklist = configuration["CONDUIT_ADMIN_IP_FILTER_BLACKLIST"]; - if (!string.IsNullOrWhiteSpace(blacklist)) - { - options.IpFiltering.Blacklist = blacklist.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()).ToList(); - } - - // Rate Limiting - options.RateLimiting.Enabled = configuration.GetValue("CONDUIT_ADMIN_RATE_LIMITING_ENABLED", false); - options.RateLimiting.MaxRequests = configuration.GetValue("CONDUIT_ADMIN_RATE_LIMIT_MAX_REQUESTS", 100); - options.RateLimiting.WindowSeconds = configuration.GetValue("CONDUIT_ADMIN_RATE_LIMIT_WINDOW_SECONDS", 60); - - var rateLimitExcluded = configuration["CONDUIT_ADMIN_RATE_LIMIT_EXCLUDED_PATHS"]; - if (!string.IsNullOrWhiteSpace(rateLimitExcluded)) - { - options.RateLimiting.ExcludedPaths = rateLimitExcluded.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()).ToList(); - } - - // Failed Authentication Protection - options.FailedAuth.Enabled = configuration.GetValue("CONDUIT_ADMIN_IP_BANNING_ENABLED", true); - options.FailedAuth.MaxAttempts = configuration.GetValue("CONDUIT_ADMIN_MAX_FAILED_AUTH_ATTEMPTS", 5); - options.FailedAuth.BanDurationMinutes = configuration.GetValue("CONDUIT_ADMIN_AUTH_BAN_DURATION_MINUTES", 30); - - // Distributed Tracking (shared with WebUI) - options.UseDistributedTracking = configuration.GetValue("CONDUIT_SECURITY_USE_DISTRIBUTED_TRACKING", true); - - // Security Headers - var headers = options.Headers; - - // X-Content-Type-Options - headers.XContentTypeOptions = configuration.GetValue("CONDUIT_ADMIN_SECURITY_HEADERS_X_CONTENT_TYPE_OPTIONS_ENABLED", true); - - // X-XSS-Protection - headers.XXssProtection = configuration.GetValue("CONDUIT_ADMIN_SECURITY_HEADERS_X_XSS_PROTECTION_ENABLED", true); - - // HSTS - headers.Hsts.Enabled = configuration.GetValue("CONDUIT_ADMIN_SECURITY_HEADERS_HSTS_ENABLED", true); - headers.Hsts.MaxAge = configuration.GetValue("CONDUIT_ADMIN_SECURITY_HEADERS_HSTS_MAX_AGE", 31536000); - - // API Authentication - options.ApiAuth.ApiKeyHeader = configuration["CONDUIT_ADMIN_API_KEY_HEADER"] ?? "X-API-Key"; - - var altHeaders = configuration["CONDUIT_ADMIN_API_KEY_ALT_HEADERS"]; - if (!string.IsNullOrWhiteSpace(altHeaders)) - { - options.ApiAuth.AlternativeHeaders = altHeaders.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()).ToList(); - } - }); - - return services; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Admin/Extensions/ServiceCollectionExtensions.cs b/ConduitLLM.Admin/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index 9075e02e7..000000000 --- a/ConduitLLM.Admin/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,248 +0,0 @@ -using ConduitLLM.Admin.Interfaces; -using ConduitLLM.Admin.Options; -using ConduitLLM.Admin.Security; -using ConduitLLM.Admin.Services; -using ConduitLLM.Configuration; // For ConduitDbContext -using ConduitLLM.Core.Extensions; // For AddMediaServices extension method -using ConduitLLM.Core.Interfaces; // For IVirtualKeyCache and ILLMClientFactory -using ConduitLLM.Configuration.Interfaces; // For repository interfaces -using ConduitLLM.Configuration.Repositories; // For repository interfaces -using ConduitLLM.Configuration.Options; - -using MassTransit; // For IPublishEndpoint -using Microsoft.AspNetCore.Authorization; -using Microsoft.EntityFrameworkCore; // For IDbContextFactory -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Options; -using StackExchange.Redis; - -namespace ConduitLLM.Admin.Extensions; - -/// -/// Extension methods for configuring Admin API services in the dependency injection container -/// -public static class ServiceCollectionExtensions -{ - /// - /// Adds all Admin API services to the dependency injection container - /// - /// The service collection - /// The application configuration - /// The service collection for chaining - public static IServiceCollection AddAdminServices(this IServiceCollection services, IConfiguration configuration) - { - // Configure security options from environment variables - services.ConfigureAdminSecurityOptions(configuration); - - // Register security service as singleton with factory to handle scoped dependencies - services.AddSingleton(serviceProvider => - { - var options = serviceProvider.GetRequiredService>(); - var config = serviceProvider.GetRequiredService(); - var logger = serviceProvider.GetRequiredService>(); - var memoryCache = serviceProvider.GetRequiredService(); - var distributedCache = serviceProvider.GetService(); // Optional - var serviceScopeFactory = serviceProvider.GetRequiredService(); - - return new SecurityService(options, config, logger, memoryCache, distributedCache, serviceScopeFactory); - }); - - // Add memory cache if not already registered - services.AddMemoryCache(); - - // Register Ephemeral Master Key Service - services.AddSingleton(); - - // Add authentication with a custom scheme - services.AddAuthentication("MasterKey") - .AddScheme("MasterKey", null); - - // Register authorization policy for master key - services.AddSingleton(); - services.AddAuthorization(options => - { - // Define the MasterKeyPolicy - options.AddPolicy("MasterKeyPolicy", policy => - policy.Requirements.Add(new MasterKeyRequirement())); - - // Set MasterKeyPolicy as the default policy for all controllers - // This ensures any controller with [Authorize] will use MasterKeyPolicy by default - options.DefaultPolicy = new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder() - .AddRequirements(new MasterKeyRequirement()) - .Build(); - }); - - // Register AdminVirtualKeyService with optional cache and event publishing dependencies - services.AddScoped(serviceProvider => - { - var virtualKeyRepository = serviceProvider.GetRequiredService(); - var spendHistoryRepository = serviceProvider.GetRequiredService(); - var groupRepository = serviceProvider.GetRequiredService(); - var cache = serviceProvider.GetService(); // Optional - null if not registered - var publishEndpoint = serviceProvider.GetService(); // Optional - null if MassTransit not configured - var logger = serviceProvider.GetRequiredService>(); - var modelProviderMappingRepository = serviceProvider.GetRequiredService(); - var modelCapabilityService = serviceProvider.GetRequiredService(); - var dbContextFactory = serviceProvider.GetRequiredService>(); - var mediaLifecycleService = serviceProvider.GetService(); // Optional - null if not configured - - return new AdminVirtualKeyService(virtualKeyRepository, spendHistoryRepository, groupRepository, cache, publishEndpoint, logger, modelProviderMappingRepository, modelCapabilityService, dbContextFactory, mediaLifecycleService); - }); - // Register AdminModelProviderMappingService with optional event publishing dependency - services.AddScoped(serviceProvider => - { - var mappingRepository = serviceProvider.GetRequiredService(); - var credentialRepository = serviceProvider.GetRequiredService(); - var modelRepository = serviceProvider.GetRequiredService(); - var publishEndpoint = serviceProvider.GetService(); // Optional - null if MassTransit not configured - var logger = serviceProvider.GetRequiredService>(); - - return new AdminModelProviderMappingService(mappingRepository, credentialRepository, modelRepository, publishEndpoint, logger); - }); - services.AddScoped(); - - // Register Analytics services - services.AddSingleton(); - services.AddScoped(); - - // Register AdminIpFilterService with optional event publishing dependency - services.AddScoped(serviceProvider => - { - var ipFilterRepository = serviceProvider.GetRequiredService(); - var ipFilterOptions = serviceProvider.GetRequiredService>(); - var publishEndpoint = serviceProvider.GetService(); // Optional - null if MassTransit not configured - var logger = serviceProvider.GetRequiredService>(); - - return new AdminIpFilterService(ipFilterRepository, ipFilterOptions, publishEndpoint, logger); - }); - services.AddScoped(); - services.AddScoped(); - // Register AdminGlobalSettingService with optional event publishing dependency - services.AddScoped(serviceProvider => - { - var globalSettingRepository = serviceProvider.GetRequiredService(); - var publishEndpoint = serviceProvider.GetService(); // Optional - null if MassTransit not configured - var logger = serviceProvider.GetRequiredService>(); - - return new AdminGlobalSettingService(globalSettingRepository, publishEndpoint, logger); - }); - // Register AdminModelCostService with optional event publishing dependency - services.AddScoped(serviceProvider => - { - var modelCostRepository = serviceProvider.GetRequiredService(); - var requestLogRepository = serviceProvider.GetRequiredService(); - var dbContextFactory = serviceProvider.GetRequiredService>(); - var publishEndpoint = serviceProvider.GetService(); // Optional - null if MassTransit not configured - var logger = serviceProvider.GetRequiredService>(); - - return new AdminModelCostService(modelCostRepository, requestLogRepository, dbContextFactory, publishEndpoint, logger); - }); - - // Register cost calculation dependencies - 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 => - { - var mediaRepository = serviceProvider.GetRequiredService(); - var mediaLifecycleService = serviceProvider.GetService(); - var logger = serviceProvider.GetRequiredService>(); - - // Only register if media lifecycle service is available - if (mediaLifecycleService == null) - { - throw new InvalidOperationException("IMediaLifecycleService must be registered to use AdminMediaService"); - } - - return new AdminMediaService(mediaRepository, mediaLifecycleService, logger); - }); - - // Register database-aware LLM client factory (must be registered before discovery service) - services.AddScoped(); - - // Configure HttpClient for discovery providers - services.AddHttpClient("DiscoveryProviders", client => - { - client.Timeout = TimeSpan.FromSeconds(30); - client.DefaultRequestHeaders.Add("User-Agent", "Conduit-LLM-Admin/1.0"); - }); - - // Model discovery providers have been removed - capabilities now come from ModelProviderMapping - - // Register Media Services using shared configuration from Core - services.AddMediaServices(configuration); - - - // Register SignalR admin notification service - services.AddScoped(); - - // Register cache management service - services.AddScoped(); - - // Register billing audit service for comprehensive billing event tracking - services.AddSingleton(); - services.AddHostedService(provider => - provider.GetRequiredService() as ConduitLLM.Configuration.Services.BillingAuditService - ?? throw new InvalidOperationException("BillingAuditService must implement IHostedService")); - - // Register Redis error store with deferred resolution - // IConnectionMultiplexer will be registered by AddRedisDataProtection in Program.cs after this method - services.AddSingleton(serviceProvider => - { - var redis = serviceProvider.GetService(); - var logger = serviceProvider.GetRequiredService>(); - - if (redis == null) - { - logger.LogError("[ConduitLLM.Admin] Redis connection not available. Redis error store will not function."); - throw new InvalidOperationException("Redis error store requires Redis. Ensure REDIS_URL or CONDUIT_REDIS_CONNECTION_STRING is configured."); - } - - logger.LogInformation("[ConduitLLM.Admin] Redis error store initialized"); - return new ConduitLLM.Core.Services.RedisErrorStore(redis, logger); - }); - - // Register provider error tracking service - services.AddSingleton(serviceProvider => - { - var errorStore = serviceProvider.GetRequiredService(); - var scopeFactory = serviceProvider.GetRequiredService(); - var logger = serviceProvider.GetRequiredService>(); - - logger.LogInformation("[ConduitLLM.Admin] Provider error tracking service initialized with Redis backend"); - return new ConduitLLM.Core.Services.ProviderErrorTrackingService(errorStore, scopeFactory, logger); - }); - - // Configure CORS for the Admin API - services.AddCors(options => - { - options.AddPolicy("AdminCorsPolicy", policy => - { - var allowedOrigins = configuration.GetSection("AdminApi:AllowedOrigins").Get(); - if (allowedOrigins != null && allowedOrigins.Length == 0) - { - policy.WithOrigins(allowedOrigins) - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials(); - } - else - { - policy.SetIsOriginAllowed(_ => true) - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials(); - } - }); - }); - - return services; - } -} 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 deleted file mode 100644 index f0e85da5b..000000000 --- a/ConduitLLM.Admin/Models/ModelCapabilities/CapabilitiesDto.cs +++ /dev/null @@ -1,217 +0,0 @@ -namespace ConduitLLM.Admin.Models.ModelCapabilities -{ - /// - /// Data transfer object representing the capabilities and characteristics of AI models. - /// - /// - /// This DTO defines what an AI model can do - its supported features, modalities, and constraints. - /// Capabilities are shared across multiple models to avoid duplication. For example, all GPT-4 - /// variants might share the same capabilities configuration. - /// - /// The capabilities system is designed to be flexible and extensible as new AI capabilities - /// emerge. Each boolean flag indicates support for a specific feature, while additional - /// properties provide configuration details like token limits and supported formats. - /// - /// Capabilities are used for: - /// - Routing decisions (finding models that support required features) - /// - Validation (ensuring requests don't exceed model limits) - /// - UI presentation (showing available features to users) - /// - Cost calculation (different capabilities may have different pricing) - /// - public class CapabilitiesDto - { - /// - /// Gets or sets the unique identifier for this capabilities configuration. - /// - /// The database-generated ID for this capabilities set. - public int Id { get; set; } - - /// - /// Gets or sets whether the model supports chat/conversation interactions. - /// - /// - /// Models with chat support can handle multi-turn conversations with message history. - /// This is the most common capability for LLMs like GPT, Claude, and Llama models. - /// - /// True if chat is supported; otherwise, false. - public bool SupportsChat { get; set; } - - /// - /// Gets or sets whether the model supports vision/image understanding. - /// - /// - /// Vision-capable models can process and understand images in addition to text. - /// Examples include GPT-4V, Claude 3 with vision, and Gemini Pro Vision. - /// These models can answer questions about images, describe visual content, etc. - /// - /// True if vision/image understanding is supported; otherwise, false. - public bool SupportsVision { get; set; } - - /// - /// Gets or sets whether the model supports function/tool calling. - /// - /// - /// Function calling allows models to invoke external tools and APIs as part of their response. - /// The model can determine when to call functions, what parameters to pass, and how to use - /// the results. This is essential for building AI agents and integrating with external systems. - /// - /// True if function calling is supported; otherwise, false. - public bool SupportsFunctionCalling { get; set; } - - /// - /// Gets or sets whether the model supports streaming responses. - /// - /// - /// Streaming allows the model to return partial responses as they're generated, - /// providing a better user experience for long responses. Most modern chat models - /// support streaming via Server-Sent Events (SSE). - /// - /// 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. - /// - /// - /// Image generation models can create images from text descriptions. - /// Examples include DALL-E, Stable Diffusion, Midjourney, and Flux. - /// These models typically support various resolutions, styles, and quality settings. - /// - /// True if image generation is supported; otherwise, false. - public bool SupportsImageGeneration { get; set; } - - /// - /// Gets or sets whether the model supports video generation. - /// - /// - /// Video generation models can create video content from text descriptions or images. - /// Examples include Runway, Pika, Stable Video Diffusion, and Sora. - /// These are typically resource-intensive and may have longer processing times. - /// - /// True if video generation is supported; otherwise, false. - public bool SupportsVideoGeneration { get; set; } - - /// - /// Gets or sets whether the model supports text embeddings generation. - /// - /// - /// Embedding models convert text into dense vector representations for semantic search, - /// similarity comparison, and retrieval-augmented generation (RAG) applications. - /// Examples include text-embedding-ada-002, voyage-ai embeddings, and cohere-embed. - /// - /// True if embeddings generation is supported; otherwise, false. - public bool SupportsEmbeddings { get; set; } - - /// - /// Gets or sets the maximum number of tokens the model can process. - /// - /// - /// This represents the model's context window - the total number of tokens it can handle - /// in a single request (input + output). Different models have different limits: - /// - GPT-3.5: 4,096 or 16,385 tokens - /// - GPT-4: 8,192 or 32,768 tokens - /// - Claude 3: 200,000 tokens - /// - Some models: 1,000,000+ tokens - /// - /// This limit is crucial for determining how much context can be provided and - /// how long the responses can be. - /// - /// The maximum token limit for the model. - public int MaxTokens { get; set; } - - /// - /// Gets or sets the minimum number of tokens for the model. - /// - /// - /// This represents the minimum tokens required for a valid request. - /// Typically set to 1 as most models can handle single-token inputs. - /// - /// The minimum token requirement. - public int MinTokens { get; set; } = 1; - - /// - /// Gets or sets the tokenizer type used by this model. - /// - /// - /// Different models use different tokenization schemes which affect how text is - /// counted and processed. Common tokenizers include: - /// - Cl100KBase (GPT-4, GPT-3.5-turbo) - /// - P50KBase (older GPT-3 models) - /// - Claude (Anthropic models) - /// - Llama (Meta Llama models) - /// - /// The tokenizer type is important for accurate token counting and cost calculation. - /// - /// 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 deleted file mode 100644 index 86845298f..000000000 --- a/ConduitLLM.Admin/Models/ModelCapabilities/CreateCapabilitiesDto.cs +++ /dev/null @@ -1,200 +0,0 @@ -namespace ConduitLLM.Admin.Models.ModelCapabilities -{ - /// - /// Data transfer object for creating a new model capabilities configuration. - /// - /// - /// Use this DTO to define a new set of capabilities that can be shared across multiple models. - /// Capabilities define what an AI model can do - its features, modalities, and constraints. - /// - /// Creating shared capability configurations helps maintain consistency and reduces - /// duplication when multiple models have identical capabilities. For example: - /// - All GPT-4 variants might share one capability set - /// - All image generation models from a provider might share another - /// - All embedding models might share a minimal capability set - /// - /// After creating a capabilities configuration, you can reference it when creating - /// or updating models. - /// - public class CreateCapabilitiesDto - { - /// - /// Gets or sets whether models with these capabilities support chat/conversation. - /// - /// - /// Set to true for language models that can handle multi-turn conversations - /// with message history. This is the primary capability for most LLMs. - /// - /// True if chat is supported; otherwise, false. - public bool SupportsChat { get; set; } - - /// - /// Gets or sets whether models support vision/image understanding. - /// - /// - /// Set to true for multimodal models that can process images alongside text. - /// Examples: GPT-4V, Claude 3 with vision, Gemini Pro Vision. - /// - /// True if vision is supported; otherwise, false. - public bool SupportsVision { get; set; } - - /// - /// Gets or sets whether models support function/tool calling. - /// - /// - /// Set to true for models that can determine when to call external functions - /// and structure the appropriate parameters. Essential for AI agents. - /// - /// True if function calling is supported; otherwise, false. - public bool SupportsFunctionCalling { get; set; } - - /// - /// Gets or sets whether models support streaming responses. - /// - /// - /// Set to true for models that can return partial responses via Server-Sent Events. - /// Most modern chat models support this for better user experience. - /// - /// 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. - /// - /// - /// Set to true for text-to-image models like DALL-E, Stable Diffusion, Midjourney. - /// These models create images from text descriptions. - /// - /// True if image generation is supported; otherwise, false. - public bool SupportsImageGeneration { get; set; } - - /// - /// Gets or sets whether models support video generation. - /// - /// - /// Set to true for text-to-video or image-to-video models like Runway, Pika, Sora. - /// These are typically resource-intensive with longer processing times. - /// - /// True if video generation is supported; otherwise, false. - public bool SupportsVideoGeneration { get; set; } - - /// - /// Gets or sets whether models support text embeddings. - /// - /// - /// Set to true for models that generate vector representations of text - /// for semantic search, similarity, and RAG applications. - /// - /// True if embeddings are supported; otherwise, false. - public bool SupportsEmbeddings { get; set; } - - /// - /// Gets or sets the maximum token context window. - /// - /// - /// Specify the total number of tokens (input + output) the model can handle. - /// Examples: - /// - 4096 for GPT-3.5 - /// - 8192 or 32768 for GPT-4 - /// - 200000 for Claude 3 - /// - 1000000+ for some specialized models - /// - /// This limit is critical for request validation and cost estimation. - /// - /// The maximum token limit. - public int MaxTokens { get; set; } - - /// - /// Gets or sets the minimum token requirement. - /// - /// - /// Typically set to 1. Only change if the model requires a minimum input length. - /// - /// The minimum token requirement. - public int MinTokens { get; set; } = 1; - - /// - /// Gets or sets the tokenizer type. - /// - /// - /// Specify the tokenization scheme for accurate token counting. - /// Common values: - /// - Cl100KBase for GPT-4/GPT-3.5-turbo - /// - P50KBase for older GPT-3 - /// - Claude for Anthropic models - /// - Llama for Meta models - /// - /// 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 deleted file mode 100644 index bef8d6e82..000000000 --- a/ConduitLLM.Admin/Models/ModelCapabilities/UpdateCapabilitiesDto.cs +++ /dev/null @@ -1,98 +0,0 @@ -namespace ConduitLLM.Admin.Models.ModelCapabilities -{ - /// - /// Data transfer object for updating an existing model capabilities configuration. - /// - /// - /// Supports partial updates - only non-null properties will be modified. - /// Changes to capabilities affect all models using this configuration, - /// so updates should be made carefully. - /// - public class UpdateCapabilitiesDto - { - /// - /// Gets or sets the ID of the capabilities configuration to update. - /// - public int Id { get; set; } - - /// - /// Gets or sets the new chat support status, or null to keep existing. - /// - public bool? SupportsChat { get; set; } - - /// - /// Gets or sets the new vision support status, or null to keep existing. - /// - public bool? SupportsVision { get; set; } - - /// - /// Gets or sets the new function calling support, or null to keep existing. - /// - public bool? SupportsFunctionCalling { get; set; } - - /// - /// Gets or sets the new streaming support, or null to keep existing. - /// - 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. - /// - public bool? SupportsImageGeneration { get; set; } - - /// - /// Gets or sets the new video generation support, or null to keep existing. - /// - public bool? SupportsVideoGeneration { get; set; } - - /// - /// Gets or sets the new embeddings support, or null to keep existing. - /// - public bool? SupportsEmbeddings { get; set; } - - /// - /// Gets or sets the new max token limit, or null to keep existing. - /// - public int? MaxTokens { get; set; } - - /// - /// Gets or sets the new min token requirement, or null to keep existing. - /// - public int? MinTokens { get; set; } - - /// - /// Gets or sets the new tokenizer type, or null to keep existing. - /// - 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/Models/Models/CreateModelDto.cs b/ConduitLLM.Admin/Models/Models/CreateModelDto.cs deleted file mode 100644 index f37d3f013..000000000 --- a/ConduitLLM.Admin/Models/Models/CreateModelDto.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace ConduitLLM.Admin.Models.Models -{ - /// - /// Data transfer object for creating a new AI model in the system. - /// - /// - /// This DTO contains the minimum required information to register a new model. - /// When creating a model, you must specify its canonical name, the series it belongs to, - /// and its capabilities configuration. The model will inherit characteristics from - /// its series and capabilities. - /// - /// Before creating a model, ensure that: - /// 1. The ModelSeries exists (or create it first) - /// 2. The ModelCapabilities configuration exists (or create it first) - /// 3. The model name is unique within the system - /// - /// Models created through this DTO will need ModelProviderMappings to be actually - /// usable by the system, as the mappings connect the canonical model to specific - /// provider implementations. - /// - public class CreateModelDto - { - /// - /// Gets or sets the canonical name of the model to create. - /// - /// - /// This should be a standardized name that uniquely identifies the model, - /// following naming conventions like "gpt-4", "claude-3-opus", "llama-3.1-70b". - /// The name should be lowercase with hyphens separating components. - /// This name will be used for model selection and routing. - /// - /// gpt-4-turbo - /// claude-3-opus-20240229 - /// The canonical model name. - public string Name { get; set; } = string.Empty; - - /// - /// Gets or sets the ID of the model series this model will belong to. - /// - /// - /// The series groups related models together and defines their author/organization. - /// For example, all GPT models belong to the "GPT" series by OpenAI, - /// all Claude models belong to the "Claude" series by Anthropic. - /// The series must exist before creating the model. - /// - /// The foreign key reference to an existing ModelSeries. - public int ModelSeriesId { get; set; } - - /// - /// Gets or sets the ID of the capabilities configuration for this model. - /// - /// - /// The capabilities define what the model can do (chat, vision, function calling, etc.). - /// Multiple models can share the same capabilities configuration. For example, - /// all GPT-4 variants might reference the same capabilities. The capabilities - /// configuration must exist before creating the model. - /// - /// The foreign key reference to an existing ModelCapabilities. - public int ModelCapabilitiesId { get; set; } - - /// - /// Gets or sets whether the model should be active upon creation. - /// - /// - /// If not specified, defaults to true. Set to false if you want to create - /// the model in an inactive state (e.g., for preparation before launch). - /// Inactive models cannot be used for requests but can still be configured - /// with provider mappings and costs. - /// - /// True to create an active model; false for inactive; null for default (true). - public bool? IsActive { get; set; } - - /// - /// Gets or sets the model-specific parameter configuration for UI generation. - /// - /// - /// This optional JSON string contains parameter definitions that override the series-level - /// parameters. When null or empty, the model uses its series' parameter configuration. - /// This allows for model-specific customization while maintaining series defaults. - /// - /// The JSON should follow the same schema as series parameters, defining UI controls - /// like sliders, selects, and inputs for model-specific parameters. - /// - /// JSON string containing parameter definitions, or null to use series defaults. - public string? ModelParameters { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Admin/Models/Models/ModelDto.cs b/ConduitLLM.Admin/Models/Models/ModelDto.cs deleted file mode 100644 index fb75acf37..000000000 --- a/ConduitLLM.Admin/Models/Models/ModelDto.cs +++ /dev/null @@ -1,123 +0,0 @@ -using ConduitLLM.Admin.Models.ModelCapabilities; -using ConduitLLM.Admin.Models.ModelSeries; - -namespace ConduitLLM.Admin.Models.Models -{ - /// - /// Data transfer object representing a canonical AI model in the system. - /// - /// - /// This DTO provides a complete view of a model including its capabilities, series information, - /// and metadata. Models represent the canonical definition of AI models available across different - /// providers (e.g., GPT-4, Claude, Llama). Each model can be offered by multiple providers through - /// ModelProviderMapping relationships. - /// - /// The model serves as the single source of truth for capabilities and characteristics, - /// independent of which provider is actually serving the model at runtime. - /// - public class ModelDto - { - /// - /// Gets or sets the unique identifier for the model. - /// - /// The database-generated ID that uniquely identifies this model across the system. - public int Id { get; set; } - - /// - /// Gets or sets the canonical name of the model. - /// - /// - /// This is the standardized name used internally to identify the model, - /// such as "gpt-4", "claude-3-opus", or "llama-3.1-70b". This name is used - /// for model selection and should be consistent across providers offering - /// the same model. - /// - /// The canonical model name. - public string Name { get; set; } = string.Empty; - - /// - /// Gets or sets the ID of the model series this model belongs to. - /// - /// - /// Models are grouped into series (e.g., GPT-4 series, Claude series, Llama series) - /// which share common characteristics and typically come from the same author/organization. - /// This relationship helps with organizing models and understanding their lineage. - /// - /// The foreign key reference to the ModelSeries entity. - public int ModelSeriesId { get; set; } - - /// - /// Gets or sets the ID of the capabilities configuration for this model. - /// - /// - /// Multiple models can share the same capabilities configuration to avoid duplication. - /// For example, all GPT-4 variants might share the same capability set. - /// - /// The foreign key reference to the ModelCapabilities entity. - public int ModelCapabilitiesId { get; set; } - - /// - /// Gets or sets the detailed capabilities of this model. - /// - /// - /// This nested object provides comprehensive information about what the model can do, - /// including support for chat, vision, function calling, streaming, and various - /// generation capabilities. This is populated when the model is fetched with details. - /// - /// The capabilities object, or null if not loaded. - public ModelCapabilitiesDto? Capabilities { get; set; } - - /// - /// Gets or sets whether this model is currently active and available for use. - /// - /// - /// Inactive models are retained in the database for historical purposes but - /// are not available for new requests. Models might be deactivated when deprecated, - /// experiencing issues, or being phased out. - /// - /// True if the model is active and available; otherwise, false. - public bool IsActive { get; set; } - - /// - /// Gets or sets the timestamp when this model was first created in the system. - /// - /// - /// This timestamp is set when the model is initially added to the database, - /// typically during seed data loading or when a new model is discovered and added. - /// - /// The UTC timestamp of model creation. - public DateTime CreatedAt { get; set; } - - /// - /// Gets or sets the timestamp when this model was last updated. - /// - /// - /// This timestamp is updated whenever any property of the model changes, - /// including activation status, capabilities, or series assignment. - /// - /// The UTC timestamp of the last update. - public DateTime UpdatedAt { get; set; } - - /// - /// Gets or sets the model series information. - /// - /// - /// This includes the series metadata like name, author, tokenizer type, - /// and importantly the UI parameters configuration. This is populated - /// when the model is fetched with details. - /// - /// The series object, or null if not loaded. - public ModelSeriesDto? Series { get; set; } - - /// - /// Gets or sets the model-specific parameter configuration for UI generation. - /// - /// - /// This JSON string contains parameter definitions that override the series-level - /// parameters. When null, the model uses its series' parameter configuration. - /// This allows for model-specific customization while maintaining series defaults. - /// - /// JSON string containing parameter definitions, or null to use series defaults. - public string? ModelParameters { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Admin/Models/Models/UpdateModelDto.cs b/ConduitLLM.Admin/Models/Models/UpdateModelDto.cs deleted file mode 100644 index 5e46ee3ff..000000000 --- a/ConduitLLM.Admin/Models/Models/UpdateModelDto.cs +++ /dev/null @@ -1,102 +0,0 @@ -namespace ConduitLLM.Admin.Models.Models -{ - /// - /// Data transfer object for updating an existing AI model in the system. - /// - /// - /// This DTO supports partial updates to model properties. Only properties that are - /// provided (non-null) will be updated. This allows for surgical updates without - /// needing to provide all model properties. - /// - /// Common update scenarios include: - /// - Activating/deactivating a model (IsActive) - /// - Changing the model's capabilities configuration (ModelCapabilitiesId) - /// - Reassigning to a different series (ModelSeriesId) - /// - Renaming a model (Name) - use with caution as it may break existing references - /// - /// Note that changing fundamental properties like the model name should be done - /// carefully as it may impact existing virtual keys, provider mappings, and - /// active requests using the model. - /// - public class UpdateModelDto - { - /// - /// Gets or sets the ID of the model to update. - /// - /// - /// This must match the ID in the request URL for validation. - /// The ID identifies which model record will be updated. - /// - /// The unique identifier of the model to update. - public int Id { get; set; } - - /// - /// Gets or sets the new canonical name for the model. - /// - /// - /// Changing the model name should be done with extreme caution as it may break: - /// - Existing virtual key configurations that reference this model - /// - Provider mappings that use the model name for routing - /// - Active client applications using the old name - /// - /// Only provide this if you intend to rename the model. Leave null to keep - /// the existing name. The new name must be unique within the system. - /// - /// The new model name, or null to keep existing. - public string? Name { get; set; } - - /// - /// Gets or sets the new model series ID. - /// - /// - /// Use this to reassign a model to a different series. This might be done - /// if the model was initially miscategorized or if series are being reorganized. - /// The new series must exist. Leave null to keep the current series assignment. - /// - /// The new series ID, or null to keep existing. - public int? ModelSeriesId { get; set; } - - /// - /// Gets or sets the new capabilities configuration ID. - /// - /// - /// Use this to change what capabilities the model advertises. This is useful when: - /// - A model gains new capabilities through provider updates - /// - Capabilities were initially misconfigured - /// - Sharing a different capability set with other models - /// - /// The new capabilities configuration must exist. Leave null to keep current capabilities. - /// - /// The new capabilities ID, or null to keep existing. - public int? ModelCapabilitiesId { get; set; } - - /// - /// Gets or sets the new activation status for the model. - /// - /// - /// Use this to activate or deactivate a model. Common scenarios: - /// - Set to false to deactivate a deprecated model - /// - Set to false temporarily when a model is experiencing issues - /// - Set to true to reactivate a previously deactivated model - /// - /// Deactivating a model prevents new requests but doesn't affect existing - /// provider mappings or cost configurations. Leave null to keep current status. - /// - /// True to activate, false to deactivate, or null to keep existing status. - public bool? IsActive { get; set; } - - /// - /// Gets or sets the model-specific parameter configuration for UI generation. - /// - /// - /// This JSON string contains parameter definitions that override the series-level - /// parameters. When null or empty, the model uses its series' parameter configuration. - /// This allows for model-specific customization while maintaining series defaults. - /// - /// The JSON should follow the same schema as series parameters, defining UI controls - /// like sliders, selects, and inputs for model-specific parameters. - /// - /// JSON string containing parameter definitions, or null to use series defaults. - public string? ModelParameters { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Admin/Program.cs b/ConduitLLM.Admin/Program.cs deleted file mode 100644 index ec38a373a..000000000 --- a/ConduitLLM.Admin/Program.cs +++ /dev/null @@ -1,356 +0,0 @@ -using System.Reflection; - -using ConduitLLM.Admin.Extensions; -using ConduitLLM.Configuration.Data; -using ConduitLLM.Configuration.Extensions; -using ConduitLLM.Core.Extensions; -using ConduitLLM.Providers.Extensions; - -using MassTransit; // Added for event bus infrastructure - -using Microsoft.OpenApi.Models; - -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; - -using Prometheus; -namespace ConduitLLM.Admin; - -/// -/// Entry point for the Admin API application -/// -public partial class Program -{ - /// - /// Application entry point that configures and starts the web application - /// - /// Command line arguments - public static async Task Main(string[] args) - { - var builder = WebApplication.CreateBuilder(args); - - // Add services to the container - builder.Services.AddControllers() - .AddJsonOptions(options => - { - // Configure JSON to use camelCase for compatibility with TypeScript clients - options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase; - options.JsonSerializerOptions.DictionaryKeyPolicy = System.Text.Json.JsonNamingPolicy.CamelCase; - - // IMPORTANT: Make JSON deserialization case-insensitive to prevent bugs - // This allows the API to accept both "initialBalance" and "InitialBalance" - options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; - }); - builder.Services.AddEndpointsApiExplorer(); - - // Add HttpClient factory for provider connection testing - builder.Services.AddHttpClient(); - - // Configure Swagger with XML comments - builder.Services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", new OpenApiInfo - { - Title = "ConduitLLM Admin API", - Version = "v1", - Description = "Administrative API for ConduitLLM", - Contact = new OpenApiContact - { - Name = "ConduitLLM Team" - } - }); - - // Use fully qualified type names to avoid schema ID conflicts - c.CustomSchemaIds(type => type.FullName?.Replace("+", ".") ?? type.Name); - - // Add XML comments - var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); - c.IncludeXmlComments(xmlPath); - - // Add security definition for API Key - c.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme - { - Type = SecuritySchemeType.ApiKey, - In = ParameterLocation.Header, - Name = "X-API-Key", - Description = "API Key Authentication" - }); - - // Add security requirement for API Key - c.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "ApiKey" - } - }, - new string[] { } - } - }); - }); - - // Add Core services - builder.Services.AddCoreServices(builder.Configuration); - - // Add Configuration services - builder.Services.AddConfigurationServices(builder.Configuration); - - // Add Provider services (needed for ILLMClientFactory) - builder.Services.AddProviderServices(); - - // Add Admin services - builder.Services.AddAdminServices(builder.Configuration); - - // Configure Data Protection with Redis persistence - // Check for REDIS_URL first, then fall back to CONDUIT_REDIS_CONNECTION_STRING - var redisUrl = Environment.GetEnvironmentVariable("REDIS_URL"); - var redisConnectionString = Environment.GetEnvironmentVariable("CONDUIT_REDIS_CONNECTION_STRING"); - - if (!string.IsNullOrEmpty(redisUrl)) - { - try - { - redisConnectionString = ConduitLLM.Configuration.Utilities.RedisUrlParser.ParseRedisUrl(redisUrl); - } - catch - { - // Failed to parse REDIS_URL, will use legacy connection string if available - } - } - - builder.Services.AddRedisDataProtection(redisConnectionString, "Conduit"); - - // Add Redis as distributed cache for ephemeral key storage - if (!string.IsNullOrEmpty(redisConnectionString)) - { - builder.Services.AddStackExchangeRedisCache(options => - { - options.Configuration = redisConnectionString; - options.InstanceName = "conduit:"; - }); - Console.WriteLine("[ConduitLLM.Admin] Distributed cache configured with Redis"); - } - else - { - // Fallback to in-memory cache if Redis is not configured - builder.Services.AddDistributedMemoryCache(); - Console.WriteLine("[ConduitLLM.Admin] WARNING: Using in-memory cache - ephemeral keys will not work across instances"); - } - - // Add SignalR with configuration - var signalRBuilder = builder.Services.AddSignalR(options => - { - options.EnableDetailedErrors = builder.Environment.IsDevelopment(); - options.ClientTimeoutInterval = TimeSpan.FromSeconds(60); - options.KeepAliveInterval = TimeSpan.FromSeconds(30); - options.MaximumReceiveMessageSize = 32 * 1024; // 32KB - options.StreamBufferCapacity = 10; - }); - - // Configure SignalR Redis backplane for horizontal scaling if Redis is configured - var signalRRedisConnectionString = builder.Configuration.GetConnectionString("RedisSignalR") ?? redisConnectionString; - if (!string.IsNullOrEmpty(signalRRedisConnectionString)) - { - signalRBuilder.AddStackExchangeRedis(signalRRedisConnectionString, options => - { - options.Configuration.ChannelPrefix = new StackExchange.Redis.RedisChannel("conduit_admin_signalr:", StackExchange.Redis.RedisChannel.PatternMode.Literal); - options.Configuration.DefaultDatabase = 3; // Separate database for Admin SignalR - }); - Console.WriteLine("[ConduitLLM.Admin] SignalR configured with Redis backplane for horizontal scaling"); - } - else - { - Console.WriteLine("[ConduitLLM.Admin] SignalR configured without Redis backplane (single-instance mode)"); - } - - // Configure RabbitMQ settings - var rabbitMqConfig = builder.Configuration.GetSection("ConduitLLM:RabbitMQ").Get() - ?? new ConduitLLM.Configuration.RabbitMqConfiguration(); - - // Check if RabbitMQ is configured - var useRabbitMq = !string.IsNullOrEmpty(rabbitMqConfig.Host) && rabbitMqConfig.Host != "localhost"; - - // Register MassTransit event bus for Admin API - builder.Services.AddMassTransit(x => - { - // Register consumers for Admin API SignalR notifications - // Provider health consumer removed - - if (useRabbitMq) - { - x.UsingRabbitMq((context, cfg) => - { - // Configure RabbitMQ connection with advanced settings - cfg.Host(new Uri($"rabbitmq://{rabbitMqConfig.Host}:{rabbitMqConfig.Port}{rabbitMqConfig.VHost}"), h => - { - h.Username(rabbitMqConfig.Username); - h.Password(rabbitMqConfig.Password); - h.Heartbeat(TimeSpan.FromSeconds(rabbitMqConfig.RequestedHeartbeat)); - - // Publisher settings - h.PublisherConfirmation = rabbitMqConfig.PublisherConfirmation; - - // Advanced connection settings for publishers - h.RequestedChannelMax(rabbitMqConfig.ChannelMax); - }); - - // Configure retry policy for publishing and consuming - cfg.UseMessageRetry(r => r.Exponential(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(2))); - - // Configure endpoints including consumers - cfg.ConfigureEndpoints(context); - }); - - Console.WriteLine($"[ConduitLLM.Admin] Event bus configured with RabbitMQ transport (multi-instance mode) - Host: {rabbitMqConfig.Host}:{rabbitMqConfig.Port}"); - Console.WriteLine("[ConduitLLM.Admin] Event publishing ENABLED - Admin services will publish:"); - Console.WriteLine(" - VirtualKeyUpdated events (triggers cache invalidation in Core API)"); - Console.WriteLine(" - VirtualKeyDeleted events (triggers cache cleanup in Core API)"); - Console.WriteLine(" - ProviderUpdated events (triggers capability refresh)"); - Console.WriteLine(" - ProviderDeleted events (triggers cache cleanup)"); - Console.WriteLine("[ConduitLLM.Admin] Event consuming ENABLED - Admin services will consume:"); - Console.WriteLine(" - ProviderHealthChanged events (forwards to Admin SignalR clients)"); - } - else - { - x.UsingInMemory((context, cfg) => - { - // NOTE: Using in-memory transport for single-instance deployments - // Configure RabbitMQ environment variables for multi-instance production - - // Configure retry policy for reliability - cfg.UseMessageRetry(r => r.Incremental(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2))); - - // Configure delayed redelivery for failed messages - cfg.UseDelayedRedelivery(r => r.Intervals(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30))); - - // Configure endpoints - cfg.ConfigureEndpoints(context); - }); - - Console.WriteLine("[ConduitLLM.Admin] Event bus configured with in-memory transport (single-instance mode)"); - Console.WriteLine("[ConduitLLM.Admin] Event publishing and consuming ENABLED - Events will be processed locally"); - Console.WriteLine("[ConduitLLM.Admin] WARNING: For production multi-instance deployments, configure RabbitMQ"); - Console.WriteLine(" - This ensures Core API instances receive cache invalidation events"); - Console.WriteLine(" - Without RabbitMQ, only the local Core API instance will be notified"); - Console.WriteLine("[ConduitLLM.Admin] Event consuming ENABLED - Admin services will consume:"); - Console.WriteLine(" - ProviderHealthChanged events (forwards to Admin SignalR clients)"); - } - }); - - // Add basic health checks - builder.Services.AddHealthChecks(); - - // Add connection pool warmer for better startup performance - builder.Services.AddHostedService(serviceProvider => - { - var logger = serviceProvider.GetRequiredService>(); - return new ConduitLLM.Core.Services.ConnectionPoolWarmer(serviceProvider, logger, "AdminAPI"); - }); - - // Configure OpenTelemetry metrics - builder.Services.AddOpenTelemetry() - .WithMetrics(meterProviderBuilder => - { - meterProviderBuilder - .SetResourceBuilder(ResourceBuilder.CreateDefault() - .AddService(serviceName: "ConduitLLM.Admin", serviceVersion: "1.0.0")) - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddMeter("System.Runtime") - .AddMeter("Microsoft.AspNetCore.Hosting") - .AddMeter("Microsoft.AspNetCore.Server.Kestrel") - .AddPrometheusExporter(); - }); - - // Add monitoring services - builder.Services.AddHostedService(); - - // Add cache infrastructure - builder.Services.AddCacheInfrastructure(builder.Configuration); - - var app = builder.Build(); - - // Log deprecation warnings and validate Redis URL - using (var scope = app.Services.CreateScope()) - { - var logger = scope.ServiceProvider.GetRequiredService>(); - ConduitLLM.Configuration.Extensions.DeprecationWarnings.LogEnvironmentVariableDeprecations(logger); - - // Validate Redis URL if provided - var envRedisUrl = Environment.GetEnvironmentVariable("REDIS_URL"); - if (!string.IsNullOrEmpty(envRedisUrl)) - { - ConduitLLM.Configuration.Services.RedisUrlValidator.ValidateAndLog(envRedisUrl, logger, "Admin Service"); - } - } - - // Run database migrations - await app.RunDatabaseMigrationAsync(); - - // Configure the HTTP request pipeline - if (app.Environment.IsDevelopment()) - { - app.UseSwagger(); - app.UseSwaggerUI(); - } - - // Only use HTTPS redirection if explicitly enabled - var enableHttpsRedirection = Environment.GetEnvironmentVariable("CONDUIT_ENABLE_HTTPS_REDIRECTION") != "false"; - if (enableHttpsRedirection) - { - app.UseHttpsRedirection(); - } - - // Add middleware for authentication and request tracking - app.UseAdminMiddleware(); - - // Enable CORS for SignalR - app.UseCors("AdminCorsPolicy"); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.MapControllers(); - - // Map SignalR hub with master key authentication (filter applied globally in AddSignalR) - app.MapHub("/hubs/admin-notifications"); - - // Map health check endpoints - app.MapHealthChecks("/health"); - app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions - { - Predicate = check => check.Tags.Contains("live") - }); - app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions - { - Predicate = check => check.Tags.Contains("ready") || check.Tags.Count == 0 - }); - - // Map Prometheus metrics endpoint - requires authentication - app.UseOpenTelemetryPrometheusScrapingEndpoint( - context => context.Request.Path == "/metrics" && - (context.User.Identity?.IsAuthenticated ?? false) - ); - - // Alternative: Map metrics endpoint without authentication (for monitoring systems) - // app.UseOpenTelemetryPrometheusScrapingEndpoint(); - - // For the prometheus-net library metrics - app.UseHttpMetrics(options => - { - options.ReduceStatusCodeCardinality(); - options.RequestDuration.Enabled = false; // We're using our custom middleware - options.RequestCount.Enabled = false; // We're using our custom middleware - }); - - app.Run(); - } -} - -// Make Program accessible for testing -public partial class Program { } diff --git a/ConduitLLM.Admin/README.md b/ConduitLLM.Admin/README.md deleted file mode 100644 index f455b57cd..000000000 --- a/ConduitLLM.Admin/README.md +++ /dev/null @@ -1,148 +0,0 @@ -# ConduitLLM Admin API - -The ConduitLLM Admin API provides a dedicated administrative interface for managing ConduitLLM configurations and resources. It centralizes all admin functionality in a single API surface, improving separation of concerns and maintainability. - -## Current Status ✅ - -The Admin API is production-ready with all major architectural issues resolved: - -1. ✅ **Standardized DTOs**: All 136+ DTOs properly centralized in ConduitLLM.Configuration.DTOs -2. ✅ **Clean Architecture**: Proper separation of concerns with no circular dependencies -3. ✅ **Complete Services**: All core services fully implemented: - - AdminVirtualKeyService - - AdminIpFilterService - - AnalyticsService (unified logs, costs, and usage analytics) - - AdminModelCostService - - AdminSystemInfoService - - Plus additional specialized services - -### ✅ Resolved Dependency Issues - -Previous dependency challenges have been successfully resolved: - -1. ✅ **No Circular Dependencies**: Clean dependency graph with proper project separation -2. ✅ **Eliminated Duplicate DTOs**: All DTOs centralized with domain-specific organization -3. ✅ **Proper Extension Methods**: All shared functionality properly abstracted - -**Current Architecture**: The Admin project now maintains clean dependencies on Configuration and Core projects only, with no WebUI dependencies required. - -## Features - -- **Virtual Keys Management**: Create, read, update, and delete virtual API keys -- **Model Provider Mapping**: Configure model-to-provider mappings -- **Router Configuration**: Manage routing rules, model deployments, and fallback configurations -- **IP Filtering**: Control access with IP whitelist/blacklist rules -- **Logs Management**: Query request logs and usage data -- **Cost Dashboard**: Track spending and view cost analytics -- **System Information**: Monitor system health and configuration - -## Getting Started - -### Prerequisites - -- .NET 9.0 SDK or later -- Access to a ConduitLLM database instance - -### Configuration - -The Admin API uses the following configuration settings: - -```json -{ - "AdminApi": { - "MasterKey": "your-master-key-here", - "AllowedOrigins": [ "http://localhost:5000", "https://localhost:5001" ] - }, - "ConnectionStrings": { - "ConfigurationDb": "..." - } -} -``` - -### Running the Admin API - -To run the Admin API: - -```bash -dotnet run --project ConduitLLM.Admin -``` - -By default, the API will be available at `https://localhost:7000`. - -## API Authentication - -The Admin API uses API key authentication. Include your master key in the `X-API-Key` header for all requests: - -``` -X-API-Key: your-master-key-here -``` - -## API Endpoints - -### Virtual Keys - -- `GET /api/virtualkeys` - List all virtual keys -- `GET /api/virtualkeys/{id}` - Get a specific virtual key -- `POST /api/virtualkeys` - Create a new virtual key -- `PUT /api/virtualkeys/{id}` - Update a virtual key -- `DELETE /api/virtualkeys/{id}` - Delete a virtual key -- `POST /api/virtualkeys/{id}/reset-spend` - Reset spend for a virtual key - -### IP Filters - -- `GET /api/ipfilter` - List all IP filters -- `GET /api/ipfilter/enabled` - List enabled IP filters -- `GET /api/ipfilter/{id}` - Get a specific IP filter -- `POST /api/ipfilter` - Create a new IP filter -- `PUT /api/ipfilter` - Update an IP filter -- `DELETE /api/ipfilter/{id}` - Delete an IP filter -- `GET /api/ipfilter/settings` - Get IP filter settings -- `PUT /api/ipfilter/settings` - Update IP filter settings - -### Cost Dashboard - -- `GET /api/costs/summary` - Get cost summary -- `GET /api/costs/trends` - Get cost trends -- `GET /api/costs/models` - Get costs by model -- `GET /api/costs/virtualkeys` - Get costs by virtual key - -### Logs - -- `GET /api/logs` - Get paginated logs -- `GET /api/logs/{id}` - Get a specific log -- `GET /api/logs/summary` - Get logs summary - -### System Info - -- `GET /api/systeminfo` - Get system information - -## Development - -### Project Structure - -- **Controllers/**: API endpoints -- **Services/**: Business logic -- **Interfaces/**: Service contracts -- **Security/**: Authentication and authorization -- **Middleware/**: Request processing -- **Extensions/**: Service configuration - -### Building - -```bash -dotnet build ConduitLLM.Admin -``` - -### Testing - -```bash -dotnet test ConduitLLM.Admin.Tests -``` - -## Integration with WebUI - -The WebUI project can be configured to use the Admin API for administrative functions instead of direct database access. This improves separation of concerns and maintainability. - -## API Documentation - -API documentation is available via Swagger at `/swagger` when running in development mode. The API also provides comprehensive XML documentation for use with tools like Swagger. \ 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/AdminIpFilterService.cs b/ConduitLLM.Admin/Services/AdminIpFilterService.cs deleted file mode 100644 index 3e16f2c75..000000000 --- a/ConduitLLM.Admin/Services/AdminIpFilterService.cs +++ /dev/null @@ -1,552 +0,0 @@ -using ConduitLLM.Admin.Interfaces; -using ConduitLLM.Configuration.Constants; -using ConduitLLM.Configuration.DTOs.IpFilter; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.Options; -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Services; - -using MassTransit; -using Microsoft.Extensions.Options; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Admin.Services; - -/// -/// Service for managing IP filters through the Admin API -/// -public class AdminIpFilterService : EventPublishingServiceBase, IAdminIpFilterService -{ - private readonly IIpFilterRepository _ipFilterRepository; - private readonly IOptionsMonitor _ipFilterOptions; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the AdminIpFilterService class - /// - /// The IP filter repository - /// The IP filter options - /// Optional event publishing endpoint (null if MassTransit not configured) - /// The logger - public AdminIpFilterService( - IIpFilterRepository ipFilterRepository, - IOptionsMonitor ipFilterOptions, - IPublishEndpoint? publishEndpoint, - ILogger logger) - : base(publishEndpoint, logger) - { - _ipFilterRepository = ipFilterRepository ?? throw new ArgumentNullException(nameof(ipFilterRepository)); - _ipFilterOptions = ipFilterOptions ?? throw new ArgumentNullException(nameof(ipFilterOptions)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - LogEventPublishingConfiguration(nameof(AdminIpFilterService)); - } - - /// - public async Task> GetAllFiltersAsync() - { - try - { - _logger.LogInformation("Getting all IP filters"); - - var filters = await _ipFilterRepository.GetAllAsync(); - return filters.Select(MapToDto); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting all IP filters"); - return Enumerable.Empty(); - } - } - - /// - public async Task> GetEnabledFiltersAsync() - { - try - { - _logger.LogInformation("Getting enabled IP filters"); - - var filters = await _ipFilterRepository.GetEnabledAsync(); - return filters.Select(MapToDto); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting enabled IP filters"); - return Enumerable.Empty(); - } - } - - /// - public async Task GetFilterByIdAsync(int id) - { - try - { - _logger.LogInformation("Getting IP filter with ID: {FilterId}", id); - - var filter = await _ipFilterRepository.GetByIdAsync(id); - return filter != null ? MapToDto(filter) : null; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting IP filter with ID {FilterId}", id); - return null; - } - } - - /// - public async Task<(bool Success, string? ErrorMessage, IpFilterDto? Filter)> CreateFilterAsync(CreateIpFilterDto createFilter) - { - try - { - _logger.LogInformation("Creating new IP filter for {IpAddress}", (createFilter.IpAddressOrCidr ?? "").Replace(Environment.NewLine, "")); - - // Validate the IP address format - if (string.IsNullOrWhiteSpace(createFilter.IpAddressOrCidr) || !IsValidIpAddressOrCidr(createFilter.IpAddressOrCidr)) - { - return (false, "Invalid IP address or CIDR format", null); - } - - // Map to entity - var entity = new IpFilterEntity - { - FilterType = createFilter.FilterType, - IpAddressOrCidr = createFilter.IpAddressOrCidr, - Description = createFilter.Description, - IsEnabled = createFilter.IsEnabled, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }; - - // Save to database - var createdFilter = await _ipFilterRepository.AddAsync(entity); - - // Publish IpFilterChanged event for cache invalidation and cross-service coordination - await PublishEventAsync( - new IpFilterChanged - { - FilterId = createdFilter.Id, - IpAddressOrCidr = createdFilter.IpAddressOrCidr, - FilterType = createdFilter.FilterType, - IsEnabled = createdFilter.IsEnabled, - ChangeType = "Created", - ChangedProperties = Array.Empty(), - Description = createdFilter.Description ?? string.Empty, - CorrelationId = Guid.NewGuid().ToString() - }, - $"create IP filter {createdFilter.Id}", - new { IpAddressOrCidr = createdFilter.IpAddressOrCidr, FilterType = createdFilter.FilterType }); - - // Return the created filter - return (true, null, MapToDto(createdFilter)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating IP filter for {IpAddress}", (createFilter.IpAddressOrCidr ?? "").Replace(Environment.NewLine, "")); - return (false, "An unexpected error occurred", null); - } - } - - /// - public async Task<(bool Success, string? ErrorMessage)> UpdateFilterAsync(UpdateIpFilterDto updateFilter) - { - try - { - _logger.LogInformation("Updating IP filter with ID: {FilterId}", updateFilter.Id); - - // Check if the filter exists - var existingFilter = await _ipFilterRepository.GetByIdAsync(updateFilter.Id); - if (existingFilter == null) - { - return (false, $"IP filter with ID {updateFilter.Id} not found"); - } - - // Validate the IP address format - if (!IsValidIpAddressOrCidr(updateFilter.IpAddressOrCidr)) - { - return (false, "Invalid IP address or CIDR format"); - } - - // Track changes for event publishing - var changedProperties = new List(); - - if (existingFilter.FilterType != updateFilter.FilterType) - { - existingFilter.FilterType = updateFilter.FilterType; - changedProperties.Add(nameof(existingFilter.FilterType)); - } - - if (existingFilter.IpAddressOrCidr != updateFilter.IpAddressOrCidr) - { - existingFilter.IpAddressOrCidr = updateFilter.IpAddressOrCidr; - changedProperties.Add(nameof(existingFilter.IpAddressOrCidr)); - } - - if (existingFilter.Description != updateFilter.Description) - { - existingFilter.Description = updateFilter.Description; - changedProperties.Add(nameof(existingFilter.Description)); - } - - if (existingFilter.IsEnabled != updateFilter.IsEnabled) - { - existingFilter.IsEnabled = updateFilter.IsEnabled; - changedProperties.Add(nameof(existingFilter.IsEnabled)); - } - - // Only proceed if there are actual changes - if (changedProperties.Count() == 0) - { - _logger.LogDebug("No changes detected for IP filter {FilterId} - skipping update", updateFilter.Id); - return (true, null); - } - - existingFilter.UpdatedAt = DateTime.UtcNow; - - // Save to database - var success = await _ipFilterRepository.UpdateAsync(existingFilter); - - if (success) - { - // Publish IpFilterChanged event for cache invalidation and cross-service coordination - await PublishEventAsync( - new IpFilterChanged - { - FilterId = existingFilter.Id, - IpAddressOrCidr = existingFilter.IpAddressOrCidr, - FilterType = existingFilter.FilterType, - IsEnabled = existingFilter.IsEnabled, - ChangeType = "Updated", - ChangedProperties = changedProperties.ToArray(), - Description = existingFilter.Description ?? string.Empty, - CorrelationId = Guid.NewGuid().ToString() - }, - $"update IP filter {existingFilter.Id}", - new { ChangedProperties = string.Join(", ", changedProperties) }); - - return (true, null); - } - else - { - return (false, "Failed to update the IP filter"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating IP filter with ID {FilterId}", updateFilter.Id); - return (false, "An unexpected error occurred"); - } - } - - /// - public async Task<(bool Success, string? ErrorMessage)> DeleteFilterAsync(int id) - { - try - { - _logger.LogInformation("Deleting IP filter with ID: {FilterId}", id); - - // Check if the filter exists - var existingFilter = await _ipFilterRepository.GetByIdAsync(id); - if (existingFilter == null) - { - return (false, $"IP filter with ID {id} not found"); - } - - // Delete from database - var success = await _ipFilterRepository.DeleteAsync(id); - - if (success) - { - // Publish IpFilterChanged event for cache invalidation and cross-service coordination - await PublishEventAsync( - new IpFilterChanged - { - FilterId = existingFilter.Id, - IpAddressOrCidr = existingFilter.IpAddressOrCidr, - FilterType = existingFilter.FilterType, - IsEnabled = existingFilter.IsEnabled, - ChangeType = "Deleted", - ChangedProperties = Array.Empty(), - Description = existingFilter.Description ?? string.Empty, - CorrelationId = Guid.NewGuid().ToString() - }, - $"delete IP filter {existingFilter.Id}", - new { IpAddressOrCidr = existingFilter.IpAddressOrCidr, FilterType = existingFilter.FilterType }); - - return (true, null); - } - else - { - return (false, "Failed to delete the IP filter"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting IP filter with ID {FilterId}", id); - return (false, "An unexpected error occurred"); - } - } - - /// - public Task GetIpFilterSettingsAsync() - { - try - { - _logger.LogInformation("Getting IP filter settings"); - - var options = _ipFilterOptions.CurrentValue; - - var settings = new IpFilterSettingsDto - { - IsEnabled = options.Enabled, - DefaultAllow = options.DefaultAllow, - BypassForAdminUi = options.BypassForAdminUi, - ExcludedEndpoints = options.ExcludedEndpoints.ToList() - }; - - return Task.FromResult(settings); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting IP filter settings"); - - // Return default settings on error - return Task.FromResult(new IpFilterSettingsDto - { - IsEnabled = false, - DefaultAllow = true, - BypassForAdminUi = true, - ExcludedEndpoints = new List { "/api/v1/health" } - }); - } - } - - /// - public Task<(bool Success, string? ErrorMessage)> UpdateIpFilterSettingsAsync(IpFilterSettingsDto settings) - { - try - { - _logger.LogInformation("Updating IP filter settings: Enabled={Enabled}, DefaultAllow={DefaultAllow}", - settings.IsEnabled, settings.DefaultAllow); - - // Validate settings - if (settings.ExcludedEndpoints == null) - { - settings.ExcludedEndpoints = new List(); - } - - // In a real implementation, we would update the appsettings.json file or a database settings table - // For now, we'll just log the settings and return success - - // Example code for updating a database-stored setting: - // await _settingsRepository.UpdateSettingAsync("IpFilter:Enabled", settings.IsEnabled.ToString()); - // await _settingsRepository.UpdateSettingAsync("IpFilter:DefaultAllow", settings.DefaultAllow.ToString()); - // await _settingsRepository.UpdateSettingAsync("IpFilter:BypassForAdminUi", settings.BypassForAdminUi.ToString()); - // await _settingsRepository.UpdateSettingAsync("IpFilter:ExcludedEndpoints", JsonSerializer.Serialize(settings.ExcludedEndpoints)); - - _logger.LogWarning("IP filter settings updated in memory only - actual settings update implementation needed"); - - return Task.FromResult<(bool Success, string? ErrorMessage)>((true, null)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating IP filter settings"); - return Task.FromResult<(bool Success, string? ErrorMessage)>((false, "An unexpected error occurred")); - } - } - - /// - public async Task CheckIpAddressAsync(string ipAddress) - { - try - { - _logger.LogInformation("Checking if IP address is allowed: {IpAddress}", ipAddress.Replace(Environment.NewLine, "")); - - // Get current IP filter settings - var settings = await GetIpFilterSettingsAsync(); - - // If IP filtering is disabled, allow all - if (!settings.IsEnabled) - { - return new IpCheckResult { IsAllowed = true }; - } - - // Validate the IP format - if (!System.Net.IPAddress.TryParse(ipAddress, out _)) - { - return new IpCheckResult - { - IsAllowed = false, - DeniedReason = "Invalid IP address format" - }; - } - - // Get all enabled IP filters - var filters = await GetEnabledFiltersAsync(); - - // Check whitelist (allow) filters - var whitelistFilters = filters.Where(f => f.FilterType == IpFilterConstants.WHITELIST).ToList(); - foreach (var filter in whitelistFilters) - { - if (IpAddressMatchesFilter(ipAddress, filter.IpAddressOrCidr)) - { - return new IpCheckResult { IsAllowed = true }; - } - } - - // Check blacklist (deny) filters - var blacklistFilters = filters.Where(f => f.FilterType == IpFilterConstants.BLACKLIST).ToList(); - foreach (var filter in blacklistFilters) - { - if (IpAddressMatchesFilter(ipAddress, filter.IpAddressOrCidr)) - { - return new IpCheckResult - { - IsAllowed = false, - DeniedReason = $"IP address matched deny filter: {filter.Description}" - }; - } - } - - // No matches found, use default policy - return new IpCheckResult - { - IsAllowed = settings.DefaultAllow, - DeniedReason = settings.DefaultAllow ? null : "IP address did not match any allow filters" - }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking if IP address is allowed: {IpAddress}", ipAddress.Replace(Environment.NewLine, "")); - - // On error, default to allowing the request (safer than potentially blocking all traffic) - return new IpCheckResult - { - IsAllowed = true, - DeniedReason = "Error during IP check, allowed as a failsafe" - }; - } - } - - /// - /// Maps an IP filter entity to a DTO - /// - /// The entity to map - /// The mapped DTO - private static IpFilterDto MapToDto(IpFilterEntity entity) - { - return new IpFilterDto - { - Id = entity.Id, - FilterType = entity.FilterType, - IpAddressOrCidr = entity.IpAddressOrCidr, - Description = entity.Description, - IsEnabled = entity.IsEnabled, - CreatedAt = entity.CreatedAt, - UpdatedAt = entity.UpdatedAt - }; - } - - /// - /// Validates if a string is a valid IP address or CIDR notation - /// - /// The string to validate - /// True if valid, false otherwise - private bool IsValidIpAddressOrCidr(string ipAddressOrCidr) - { - if (string.IsNullOrWhiteSpace(ipAddressOrCidr)) - { - return false; - } - - try - { - // Check if it's a CIDR notation (e.g., 192.168.1.0/24) - if (ipAddressOrCidr.Contains('/')) - { - var parts = ipAddressOrCidr.Split('/'); - if (parts.Length != 2) - { - return false; - } - - // Validate IP part - if (!System.Net.IPAddress.TryParse(parts[0], out _)) - { - return false; - } - - // Validate prefix length - if (!int.TryParse(parts[1], out int prefixLength)) - { - return false; - } - - // For IPv4, prefix length should be between 0 and 32 - // For IPv6, prefix length should be between 0 and 128 - // We'll accept 0-128 for simplicity - return prefixLength >= 0 && prefixLength <= 128; - } - else - { - // It's a simple IP address - return System.Net.IPAddress.TryParse(ipAddressOrCidr, out _); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Error validating IP address or CIDR: {IpAddressOrCidr}", ipAddressOrCidr.Replace(Environment.NewLine, "")); - return false; - } - } - - /// - /// Checks if an IP address matches a filter (exact match or CIDR range) - /// - /// The IP address to check - /// The filter value (IP address or CIDR notation) - /// True if the IP matches the filter, false otherwise - private bool IpAddressMatchesFilter(string ipAddress, string filterValue) - { - // Simple exact match - if (ipAddress == filterValue) - { - return true; - } - - // If the filter is a CIDR range - if (filterValue.Contains('/')) - { - try - { - // This is a simplified implementation that would need to be replaced - // with actual CIDR range matching logic in a production environment - - var parts = filterValue.Split('/'); - if (parts.Length != 2) - { - return false; - } - - var networkAddress = parts[0]; - - // For a very basic check, see if the IP starts with the same network portion - // This is NOT accurate for real CIDR matching and is just a placeholder - return ipAddress.StartsWith(networkAddress.Substring(0, networkAddress.LastIndexOf('.'))); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Error checking IP {IpAddress} against CIDR {CidrRange}", ipAddress.Replace(Environment.NewLine, ""), filterValue.Replace(Environment.NewLine, "")); - return false; - } - } - - return false; - } - - /// - public async Task IsIpAllowedAsync(string ipAddress) - { - var result = await CheckIpAddressAsync(ipAddress); - return result.IsAllowed; - } -} diff --git a/ConduitLLM.Admin/Services/AdminModelCostService.Parsers.cs b/ConduitLLM.Admin/Services/AdminModelCostService.Parsers.cs deleted file mode 100644 index 158670eca..000000000 --- a/ConduitLLM.Admin/Services/AdminModelCostService.Parsers.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System.Text; -using System.Text.Json; - -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Admin.Services -{ - /// - /// Service implementation for managing model costs - CSV/JSON parsing functionality - /// - public partial class AdminModelCostService - { - private string GenerateJsonExport(List modelCosts) - { - var exportData = modelCosts.Select(mc => new ModelCostExportDto - { - CostName = mc.CostName, - PricingModel = mc.PricingModel, - PricingConfiguration = mc.PricingConfiguration, - InputCostPerMillionTokens = mc.InputCostPerMillionTokens, - 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, - BatchProcessingMultiplier = mc.BatchProcessingMultiplier, - SupportsBatchProcessing = mc.SupportsBatchProcessing, - CostPerSearchUnit = mc.CostPerSearchUnit, - CostPerInferenceStep = mc.CostPerInferenceStep, - DefaultInferenceSteps = mc.DefaultInferenceSteps - }); - - return JsonSerializer.Serialize(exportData, new JsonSerializerOptions - { - WriteIndented = true - }); - } - - 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"); - - foreach (var modelCost in modelCosts.OrderBy(mc => mc.CostName)) - { - csv.AppendLine($"{EscapeCsvValue(modelCost.CostName)}," + - $"{modelCost.PricingModel}," + - $"{EscapeCsvValue(modelCost.PricingConfiguration ?? "")}," + - $"{modelCost.InputCostPerMillionTokens:F6}," + - $"{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 ?? "")}," + - $"{(modelCost.BatchProcessingMultiplier?.ToString("F4") ?? "")}," + - $"{(modelCost.SupportsBatchProcessing ? "Yes" : "No")}," + - $"{(modelCost.CostPerSearchUnit?.ToString("F6") ?? "")}," + - $"{(modelCost.CostPerInferenceStep?.ToString("F6") ?? "")}," + - $"{(modelCost.DefaultInferenceSteps?.ToString() ?? "")}"); - } - - return csv.ToString(); - } - - private List ParseJsonImport(string jsonData) - { - try - { - var importData = JsonSerializer.Deserialize>(jsonData); - if (importData == null) return new List(); - - return importData.Select(d => new CreateModelCostDto - { - CostName = d.CostName, - PricingModel = d.PricingModel, - PricingConfiguration = d.PricingConfiguration, - InputCostPerMillionTokens = d.InputCostPerMillionTokens, - 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, - BatchProcessingMultiplier = d.BatchProcessingMultiplier, - SupportsBatchProcessing = d.SupportsBatchProcessing, - CostPerSearchUnit = d.CostPerSearchUnit, - CostPerInferenceStep = d.CostPerInferenceStep, - DefaultInferenceSteps = d.DefaultInferenceSteps - }).ToList(); - } - catch (JsonException ex) - { - _logger.LogError(ex, "Failed to parse JSON import data"); - throw new ArgumentException("Invalid JSON format", ex); - } - } - - private List ParseCsvImport(string csvData) - { - var modelCosts = 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 < 2) - { - _logger.LogWarning("Skipping invalid CSV line: {Line}", lines[i].Replace(Environment.NewLine, "")); - continue; - } - - try - { - var modelCost = new CreateModelCostDto - { - CostName = UnescapeCsvValue(parts[0]), - PricingModel = parts.Length > 1 && Enum.TryParse(parts[1], out var pricingModel) ? pricingModel : PricingModel.Standard, - PricingConfiguration = parts.Length > 2 ? UnescapeCsvValue(parts[2]) : null, - InputCostPerMillionTokens = parts.Length > 3 && decimal.TryParse(parts[3], out var inputCost) ? inputCost : 0, - 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 - }; - - modelCosts.Add(modelCost); - } - 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 modelCosts; - } - - private static string EscapeCsvValue(string value) - { - if (string.IsNullOrEmpty(value)) - return ""; - - if (value.Contains(',') || value.Contains('"') || value.Contains('\n') || value.Contains('\r')) - { - return $"\"{value.Replace("\"", "\"\"")}\""; - } - - return value; - } - - private static string UnescapeCsvValue(string value) - { - if (string.IsNullOrEmpty(value)) - return ""; - - if (value.StartsWith("\"") && value.EndsWith("\"")) - { - value = value.Substring(1, value.Length - 2); - value = value.Replace("\"\"", "\""); - } - - return value; - } - } -} \ No newline at end of file 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/docs/SECURITY-ARCHITECTURE.md b/ConduitLLM.Admin/docs/SECURITY-ARCHITECTURE.md deleted file mode 100644 index 322b798f3..000000000 --- a/ConduitLLM.Admin/docs/SECURITY-ARCHITECTURE.md +++ /dev/null @@ -1,202 +0,0 @@ -# Admin API Security Architecture - -## Overview - -The Admin API has been enhanced with a comprehensive security architecture that provides multiple layers of protection while maintaining compatibility with existing API clients. - -## Security Features - -### 1. Unified Security Middleware - -A single `SecurityMiddleware` handles all security checks in one pass: -- API key authentication -- IP filtering (environment and database-based) -- Rate limiting -- Failed authentication protection - -### 2. API Key Authentication - -- Primary header: `X-API-Key` (configurable) -- Alternative headers: `X-Master-Key` (backward compatibility) -- Uses `CONDUIT_API_TO_API_BACKEND_AUTH_KEY` environment variable -- Supports Ephemeral Master Keys (single-use, short-lived) -- Failed authentication attempts are tracked - -### 3. IP Filtering - -**Environment-based filtering:** -- Whitelist/blacklist with CIDR support -- Private IP detection (RFC 1918) -- Configurable modes: permissive (blacklist) or restrictive (whitelist) - -**Database-based filtering:** -- Integrates with existing IP filter service -- Managed through Admin API endpoints - -### 4. Rate Limiting - -- Sliding window algorithm -- Configurable per-IP limits -- Excludes health and Swagger endpoints -- Returns appropriate headers (Retry-After, X-RateLimit-Limit) - -### 5. Failed Authentication Protection - -- Automatic IP banning after threshold -- Configurable ban duration -- Shared tracking with WebUI via Redis - -### 6. Security Headers - -- X-Content-Type-Options: nosniff -- X-XSS-Protection: 1; mode=block -- Strict-Transport-Security (HTTPS only) -- Removes server identification headers - -### 7. Ephemeral Master Key (EMK) - -Provides one-time, short-lived authentication for Admin API operations and secure WebUI flows. - -Key properties: -- Single-use tokens with 5-minute TTL -- Stored in Redis with automatic expiration and cleanup -- Token format: `emk_...` (URL-safe) - -Generate an EMK: - -```bash -curl -X POST "$ADMIN_API_BASE_URL/api/admin/auth/ephemeral-master-key" \ - -H "X-API-Key: $CONDUIT_API_TO_API_BACKEND_AUTH_KEY" -``` - -Response: - -```json -{ - "ephemeralMasterKey": "emk_abc...", - "expiresAt": "2025-01-01T00:00:00Z" -} -``` - -Use the EMK for the next request: - -```bash -curl -H "X-API-Key: emk_abc..." "$ADMIN_API_BASE_URL/api/providers" -``` - -Validation behavior: -- Non-streaming requests: validated and marked consumed; key cannot be reused -- Streaming (e.g., SignalR/SSE): key is consumed and deleted once the connection is established; the connection remains authorized - -Implementation notes: -- Backed by distributed cache with key prefix `ephemeral:master:` -- Authentication handler accepts EMK via `X-API-Key` or `X-Master-Key` - -## Configuration - -### Environment Variables - -```bash -# IP Filtering -CONDUIT_ADMIN_IP_FILTERING_ENABLED=true -CONDUIT_ADMIN_IP_FILTER_MODE=permissive -CONDUIT_ADMIN_IP_FILTER_ALLOW_PRIVATE=true -CONDUIT_ADMIN_IP_FILTER_WHITELIST=192.168.1.0/24,10.0.0.0/8 -CONDUIT_ADMIN_IP_FILTER_BLACKLIST=192.168.1.100 - -# Rate Limiting -CONDUIT_ADMIN_RATE_LIMITING_ENABLED=true -CONDUIT_ADMIN_RATE_LIMIT_MAX_REQUESTS=100 -CONDUIT_ADMIN_RATE_LIMIT_WINDOW_SECONDS=60 -CONDUIT_ADMIN_RATE_LIMIT_EXCLUDED_PATHS=/health,/swagger - -# Failed Authentication Protection -CONDUIT_ADMIN_IP_BANNING_ENABLED=true -CONDUIT_ADMIN_MAX_FAILED_AUTH_ATTEMPTS=5 -CONDUIT_ADMIN_AUTH_BAN_DURATION_MINUTES=30 - -# Distributed Tracking (shared with WebUI) -CONDUIT_SECURITY_USE_DISTRIBUTED_TRACKING=true - -# Security Headers -CONDUIT_ADMIN_SECURITY_HEADERS_X_CONTENT_TYPE_OPTIONS_ENABLED=true -CONDUIT_ADMIN_SECURITY_HEADERS_X_XSS_PROTECTION_ENABLED=true -CONDUIT_ADMIN_SECURITY_HEADERS_HSTS_ENABLED=true -CONDUIT_ADMIN_SECURITY_HEADERS_HSTS_MAX_AGE=31536000 - -# API Authentication (header names) -CONDUIT_ADMIN_API_KEY_HEADER=X-API-Key -CONDUIT_ADMIN_API_KEY_ALT_HEADERS=X-Master-Key -``` - -## Shared Security Tracking - -The Admin API shares security tracking data with WebUI through Redis: - -### Redis Key Structure - -``` -rate_limit:admin-api:{ip} - Rate limiting counters -failed_login:{ip} - Failed authentication attempts -ban:{ip} - Banned IPs -``` - -### Data Format - -```json -{ - "bannedUntil": "2024-01-20T10:30:00Z", - "failedAttempts": 5, - "source": "admin-api", - "reason": "Exceeded max failed authentication attempts" -} -``` - -## Integration with WebUI Security Dashboard - -The WebUI Security Dashboard can display: -- Combined banned IPs from both services -- Failed authentication attempts across services -- Rate limiting statistics per service -- Source identification for security events - - - -## Backward Compatibility - -- Existing API clients continue to work without changes -- `X-API-Key` header is still supported -- `X-Master-Key` header supported for legacy clients -- Same authentication flow, enhanced with security features - -## Performance Considerations - -- Single middleware pass for all security checks -- Efficient Redis caching for distributed tracking -- Minimal overhead for authenticated requests -- Excluded paths bypass unnecessary checks - -## Security Best Practices - -1. **Enable IP Filtering**: Restrict access to known IP ranges -2. **Set Rate Limits**: Prevent API abuse -3. **Monitor Failed Attempts**: Review security dashboard regularly -4. **Use HTTPS**: Enable HSTS headers for secure transport -5. **Rotate API Keys**: Change `CONDUIT_API_TO_API_BACKEND_AUTH_KEY` periodically - -## Troubleshooting - -### Common Issues - -1. **403 Forbidden**: Check IP filtering rules -2. **429 Too Many Requests**: Rate limit exceeded -3. **401 Unauthorized**: Invalid or missing API key -4. **Banned IP**: Check failed authentication attempts - -### Debug Mode - -Enable debug logging to see security decisions: -```bash -ASPNETCORE_ENVIRONMENT=Development -Logging__LogLevel__ConduitLLM.Admin.Services.SecurityService=Debug -``` \ No newline at end of file diff --git a/ConduitLLM.Configuration/ConduitLLM.Configuration.csproj b/ConduitLLM.Configuration/ConduitLLM.Configuration.csproj deleted file mode 100644 index bd8ea0bac..000000000 --- a/ConduitLLM.Configuration/ConduitLLM.Configuration.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - net9.0 - enable - enable - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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/GlobalSettingDto.cs b/ConduitLLM.Configuration/DTOs/GlobalSettingDto.cs deleted file mode 100644 index 2e9d7d6ee..000000000 --- a/ConduitLLM.Configuration/DTOs/GlobalSettingDto.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Configuration.DTOs -{ - /// - /// Data transfer object for global settings - /// - public class GlobalSettingDto - { - /// - /// Unique identifier for the setting - /// - public int Id { get; set; } - - /// - /// Setting key - /// - [Required] - [MaxLength(100)] - public string Key { get; set; } = string.Empty; - - /// - /// Setting value - /// - [Required] - [MaxLength(2000)] - public string Value { get; set; } = string.Empty; - - /// - /// Optional description of the setting - /// - [MaxLength(500)] - public string? Description { get; set; } - - /// - /// Date when the setting was created - /// - public DateTime CreatedAt { get; set; } - - /// - /// Date when the setting was last updated - /// - public DateTime UpdatedAt { get; set; } - } - - /// - /// Data transfer object for creating a global setting - /// - public class CreateGlobalSettingDto - { - /// - /// Setting key - /// - [Required] - [MaxLength(100)] - public string Key { get; set; } = string.Empty; - - /// - /// Setting value - /// - [Required] - [MaxLength(2000)] - public string Value { get; set; } = string.Empty; - - /// - /// Optional description of the setting - /// - [MaxLength(500)] - public string? Description { get; set; } - } - - /// - /// Data transfer object for updating a global setting - /// - public class UpdateGlobalSettingDto - { - /// - /// Unique identifier for the setting - /// - public int Id { get; set; } - - /// - /// Setting value - /// - [Required] - [MaxLength(2000)] - public string Value { get; set; } = string.Empty; - - /// - /// Optional description of the setting - /// - [MaxLength(500)] - public string? Description { get; set; } - } - - /// - /// Data transfer object for updating a global setting by key - /// - public class UpdateGlobalSettingByKeyDto - { - /// - /// Setting key - /// - [Required] - [MaxLength(100)] - public string Key { get; set; } = string.Empty; - - /// - /// Setting value - /// - [Required] - [MaxLength(2000)] - public string Value { get; set; } = string.Empty; - - /// - /// Optional description of the setting - /// - [MaxLength(500)] - public string? Description { get; set; } - } -} diff --git a/ConduitLLM.Configuration/DTOs/Metrics/ProviderHealthStatus.cs b/ConduitLLM.Configuration/DTOs/Metrics/ProviderHealthStatus.cs deleted file mode 100644 index a70bf917c..000000000 --- a/ConduitLLM.Configuration/DTOs/Metrics/ProviderHealthStatus.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace ConduitLLM.Configuration.DTOs.Metrics -{ - /// - /// Provider health status. - /// - public class ProviderHealthStatus - { - /// - /// Provider type. - /// - public ProviderType ProviderType { get; set; } - - /// - /// Health status: healthy, degraded, or unhealthy. - /// - public string Status { get; set; } = "healthy"; - - /// - /// Last successful request timestamp. - /// - public DateTime? LastSuccessfulRequest { get; set; } - - /// - /// Error rate percentage. - /// - public double ErrorRate { get; set; } - - /// - /// Average latency in milliseconds. - /// - public double AverageLatency { get; set; } - - /// - /// Number of available models. - /// - public int AvailableModels { get; set; } - - /// - /// Is enabled. - /// - public bool IsEnabled { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Configuration/DTOs/ModelCostDto.Create.cs b/ConduitLLM.Configuration/DTOs/ModelCostDto.Create.cs deleted file mode 100644 index f2a5c2270..000000000 --- a/ConduitLLM.Configuration/DTOs/ModelCostDto.Create.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Configuration.DTOs -{ - /// - /// Data transfer object for creating a model cost entry - /// - public class CreateModelCostDto - { - /// - /// User-friendly name for this cost configuration - /// - /// - /// Examples: "GPT-4 Standard Pricing", "Llama 3 Unified Cost", "Embedding Models - Ada" - /// - [Required] - [MaxLength(255)] - public string CostName { get; set; } = string.Empty; - - /// - /// The pricing model type that determines how costs are calculated - /// - [Required] - public PricingModel PricingModel { get; set; } = PricingModel.Standard; - - /// - /// JSON configuration for complex pricing models - /// - public string? PricingConfiguration { get; set; } - - /// - /// List of model mapping IDs to associate with this cost - /// - /// - /// These are the IDs of ModelProviderMapping entities that should use this cost configuration. - /// - public List ModelProviderMappingIds { get; set; } = new List(); - - /// - /// Model type for categorization - /// - [Required] - [MaxLength(50)] - public string ModelType { get; set; } = "chat"; - - /// - /// Priority value for pattern matching - /// - public int Priority { get; set; } = 0; - - /// - /// Optional description - /// - [MaxLength(500)] - public string? Description { get; set; } - - /// - /// Cost per million input tokens for chat/completion requests in USD - /// - [Range(0, double.MaxValue)] - public decimal InputCostPerMillionTokens { get; set; } = 0; - - /// - /// Cost per million output tokens for chat/completion requests in USD - /// - [Range(0, double.MaxValue)] - public decimal OutputCostPerMillionTokens { get; set; } = 0; - - /// - /// Cost per million tokens for embedding requests in USD, if applicable - /// - public decimal? EmbeddingCostPerMillionTokens { get; set; } - - /// - /// Cost per image for image generation requests in USD, if applicable - /// - 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 - /// - public decimal? VideoCostPerSecond { get; set; } - - /// - /// Resolution-based cost multipliers for video generation as JSON string - /// - /// - /// JSON object containing resolution-to-multiplier mappings. - /// Example: {"720p": 1.0, "1080p": 1.5, "4k": 2.5} - /// - public string? VideoResolutionMultipliers { get; set; } - - /// - /// Cost multiplier for batch processing operations, if applicable - /// - /// - /// This represents a cost reduction factor for batch API usage. - /// Example: 0.5 means 50% discount (half price), 0.6 means 40% discount. - /// Applied to the standard token costs when requests are processed through batch APIs. - /// - public decimal? BatchProcessingMultiplier { get; set; } - - /// - /// Indicates if this model supports batch processing - /// - /// - /// When true, requests can be processed through batch endpoints with the BatchProcessingMultiplier discount applied. - /// - public bool SupportsBatchProcessing { get; set; } - - /// - /// Quality-based cost multipliers for image generation as JSON string - /// - /// - /// JSON object containing quality-to-multiplier mappings. - /// Example: {"standard": 1.0, "hd": 2.0} - /// - public string? ImageQualityMultipliers { get; set; } - - /// - /// Resolution-based cost multipliers for image generation as JSON string - /// - /// - /// JSON object containing resolution-to-multiplier mappings. - /// Example: {"1024x1024": 1.0, "1792x1024": 1.5, "1024x1792": 1.5} - /// - public string? ImageResolutionMultipliers { get; set; } - - /// - /// Cost per million cached input tokens for prompt caching in USD, if applicable - /// - /// - /// This represents the cost for processing one million cached input tokens (reading from cache). - /// Used by providers like Anthropic Claude and Google Gemini that offer prompt caching. - /// Typically much lower than standard input token costs (e.g., 10% of regular cost). - /// - public decimal? CachedInputCostPerMillionTokens { get; set; } - - /// - /// Cost per million tokens for writing to the prompt cache in USD, if applicable - /// - /// - /// This represents the cost for writing one million tokens to the prompt cache. - /// Used by providers like Anthropic Claude and Google Gemini that offer prompt caching. - /// The write cost is incurred when new content is added to the cache. - /// - public decimal? CachedInputWriteCostPerMillionTokens { get; set; } - - /// - /// Cost per search unit for reranking models in USD per 1000 units, if applicable - /// - /// - /// Used by reranking models like Cohere Rerank that charge per search unit rather than per token. - /// A search unit typically consists of 1 query + up to 100 documents to be ranked. - /// Documents over 500 tokens are split into chunks, each counting as a separate document. - /// - public decimal? CostPerSearchUnit { get; set; } - - /// - /// Cost per inference step for image generation models in USD, if applicable - /// - /// - /// Used by providers like Fireworks that charge based on the number of iterative refinement steps. - /// Different models require different numbers of steps to generate an image. - /// Example: FLUX.1[schnell] uses 4 steps × $0.00035/step = $0.0014 per image. - /// Example: SDXL typically uses 30 steps × $0.00013/step = $0.0039 per image. - /// - public decimal? CostPerInferenceStep { get; set; } - - /// - /// Default number of inference steps for this model - /// - /// - /// Indicates the standard number of iterative refinement steps this model uses for image generation. - /// Used when the client request doesn't specify a custom step count. - /// Example: FLUX.1[schnell] uses 4 steps for fast generation, SDXL uses 30 steps for higher quality. - /// - public int? DefaultInferenceSteps { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Configuration/DTOs/ModelCostDto.Update.cs b/ConduitLLM.Configuration/DTOs/ModelCostDto.Update.cs deleted file mode 100644 index 7e2da1221..000000000 --- a/ConduitLLM.Configuration/DTOs/ModelCostDto.Update.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Configuration.DTOs -{ - /// - /// Data transfer object for updating a model cost entry - /// - public class UpdateModelCostDto - { - /// - /// Unique identifier for the model cost entry - /// - public int Id { get; set; } - - /// - /// User-friendly name for this cost configuration - /// - /// - /// Examples: "GPT-4 Standard Pricing", "Llama 3 Unified Cost", "Embedding Models - Ada" - /// - [Required] - [MaxLength(255)] - public string CostName { get; set; } = string.Empty; - - /// - /// The pricing model type that determines how costs are calculated - /// - [Required] - public PricingModel PricingModel { get; set; } = PricingModel.Standard; - - /// - /// JSON configuration for complex pricing models - /// - public string? PricingConfiguration { get; set; } - - /// - /// List of model mapping IDs to associate with this cost - /// - /// - /// These are the IDs of ModelProviderMapping entities that should use this cost configuration. - /// - public List ModelProviderMappingIds { get; set; } = new List(); - - /// - /// Model type for categorization - /// - [Required] - [MaxLength(50)] - public string ModelType { get; set; } = "chat"; - - /// - /// Priority value for pattern matching - /// - public int Priority { get; set; } = 0; - - /// - /// Optional description - /// - [MaxLength(500)] - public string? Description { get; set; } - - /// - /// Indicates whether this cost configuration is active - /// - public bool IsActive { get; set; } = true; - - /// - /// Cost per million input tokens for chat/completion requests in USD - /// - [Range(0, double.MaxValue)] - public decimal InputCostPerMillionTokens { get; set; } = 0; - - /// - /// Cost per million output tokens for chat/completion requests in USD - /// - [Range(0, double.MaxValue)] - public decimal OutputCostPerMillionTokens { get; set; } = 0; - - /// - /// Cost per million tokens for embedding requests in USD, if applicable - /// - public decimal? EmbeddingCostPerMillionTokens { get; set; } - - /// - /// Cost per image for image generation requests in USD, if applicable - /// - 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 - /// - public decimal? VideoCostPerSecond { get; set; } - - /// - /// Resolution-based cost multipliers for video generation as JSON string - /// - /// - /// JSON object containing resolution-to-multiplier mappings. - /// Example: {"720p": 1.0, "1080p": 1.5, "4k": 2.5} - /// - public string? VideoResolutionMultipliers { get; set; } - - /// - /// Cost multiplier for batch processing operations, if applicable - /// - /// - /// This represents a cost reduction factor for batch API usage. - /// Example: 0.5 means 50% discount (half price), 0.6 means 40% discount. - /// Applied to the standard token costs when requests are processed through batch APIs. - /// - public decimal? BatchProcessingMultiplier { get; set; } - - /// - /// Indicates if this model supports batch processing - /// - /// - /// When true, requests can be processed through batch endpoints with the BatchProcessingMultiplier discount applied. - /// - public bool SupportsBatchProcessing { get; set; } - - /// - /// Quality-based cost multipliers for image generation as JSON string - /// - /// - /// JSON object containing quality-to-multiplier mappings. - /// Example: {"standard": 1.0, "hd": 2.0} - /// - public string? ImageQualityMultipliers { get; set; } - - /// - /// Resolution-based cost multipliers for image generation as JSON string - /// - /// - /// JSON object containing resolution-to-multiplier mappings. - /// Example: {"1024x1024": 1.0, "1792x1024": 1.5, "1024x1792": 1.5} - /// - public string? ImageResolutionMultipliers { get; set; } - - /// - /// Cost per million cached input tokens for prompt caching in USD, if applicable - /// - /// - /// This represents the cost for processing one million cached input tokens (reading from cache). - /// Used by providers like Anthropic Claude and Google Gemini that offer prompt caching. - /// Typically much lower than standard input token costs (e.g., 10% of regular cost). - /// - public decimal? CachedInputCostPerMillionTokens { get; set; } - - /// - /// Cost per million tokens for writing to the prompt cache in USD, if applicable - /// - /// - /// This represents the cost for writing one million tokens to the prompt cache. - /// Used by providers like Anthropic Claude and Google Gemini that offer prompt caching. - /// The write cost is incurred when new content is added to the cache. - /// - public decimal? CachedInputWriteCostPerMillionTokens { get; set; } - - /// - /// Cost per search unit for reranking models in USD per 1000 units, if applicable - /// - /// - /// Used by reranking models like Cohere Rerank that charge per search unit rather than per token. - /// A search unit typically consists of 1 query + up to 100 documents to be ranked. - /// Documents over 500 tokens are split into chunks, each counting as a separate document. - /// - public decimal? CostPerSearchUnit { get; set; } - - /// - /// Cost per inference step for image generation models in USD, if applicable - /// - /// - /// Used by providers like Fireworks that charge based on the number of iterative refinement steps. - /// Different models require different numbers of steps to generate an image. - /// Example: FLUX.1[schnell] uses 4 steps × $0.00035/step = $0.0014 per image. - /// Example: SDXL typically uses 30 steps × $0.00013/step = $0.0039 per image. - /// - public decimal? CostPerInferenceStep { get; set; } - - /// - /// Default number of inference steps for this model - /// - /// - /// Indicates the standard number of iterative refinement steps this model uses for image generation. - /// Used when the client request doesn't specify a custom step count. - /// Example: FLUX.1[schnell] uses 4 steps for fast generation, SDXL uses 30 steps for higher quality. - /// - public int? DefaultInferenceSteps { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Configuration/DTOs/ModelCostDto.cs b/ConduitLLM.Configuration/DTOs/ModelCostDto.cs deleted file mode 100644 index 8376cedac..000000000 --- a/ConduitLLM.Configuration/DTOs/ModelCostDto.cs +++ /dev/null @@ -1,244 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Configuration.DTOs -{ - /// - /// Data transfer object for model cost information - /// - public class ModelCostDto - { - /// - /// Unique identifier for the model cost entry - /// - public int Id { get; set; } - - /// - /// User-friendly name for this cost configuration - /// - /// - /// Examples: "GPT-4 Standard Pricing", "Llama 3 Unified Cost", "Embedding Models - Ada" - /// - [Required] - [MaxLength(255)] - public string CostName { get; set; } = string.Empty; - - /// - /// The pricing model type that determines how costs are calculated - /// - [Required] - public PricingModel PricingModel { get; set; } = PricingModel.Standard; - - /// - /// JSON configuration for complex pricing models - /// - /// - /// Structure depends on PricingModel: - /// - PerVideo: {"rates": {"512p_6": 0.10, "720p_10": 0.15}} - /// - PerSecondVideo: {"baseRate": 0.09, "resolutionMultipliers": {"720p": 1.0}} - /// - InferenceSteps: {"costPerStep": 0.00013, "defaultSteps": 30} - /// - TieredTokens: {"tiers": [{"maxContext": 200000, "inputCost": 400}]} - /// - public string? PricingConfiguration { get; set; } - - /// - /// List of model aliases that use this cost configuration - /// - /// - /// This is populated from the ModelCostMappings relationship. - /// Shows which models are associated with this cost configuration. - /// - public List AssociatedModelAliases { get; set; } = new List(); - - /// - /// Cost per million input tokens for chat/completion requests in USD - /// - [Range(0, double.MaxValue)] - public decimal InputCostPerMillionTokens { get; set; } = 0; - - /// - /// Cost per million output tokens for chat/completion requests in USD - /// - [Range(0, double.MaxValue)] - public decimal OutputCostPerMillionTokens { get; set; } = 0; - - /// - /// Cost per million tokens for embedding requests in USD, if applicable - /// - public decimal? EmbeddingCostPerMillionTokens { get; set; } - - /// - /// Cost per image for image generation requests in USD, if applicable - /// - public decimal? ImageCostPerImage { get; set; } - - /// - /// Creation timestamp of this cost record - /// - public DateTime CreatedAt { get; set; } - - /// - /// Last update timestamp of this cost record - /// - public DateTime UpdatedAt { get; set; } - - /// - /// Model type for categorization - /// - /// - /// Indicates the type of operations this model cost applies to (chat, embedding, image, audio, video). - /// - [Required] - [MaxLength(50)] - public string ModelType { get; set; } = "chat"; - - /// - /// Indicates whether this cost configuration is active - /// - public bool IsActive { get; set; } = true; - - /// - /// Effective date for this pricing - /// - public DateTime EffectiveDate { get; set; } - - /// - /// Optional expiry date for this pricing - /// - public DateTime? ExpiryDate { get; set; } - - /// - /// Optional description for this model cost entry - /// - [MaxLength(500)] - public string? Description { get; set; } - - /// - /// Priority value for this model cost entry - /// - /// - /// Higher priority patterns are evaluated first when matching model names. - /// - 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 - /// - public decimal? VideoCostPerSecond { get; set; } - - /// - /// Resolution-based cost multipliers for video generation as JSON string - /// - /// - /// JSON object containing resolution-to-multiplier mappings. - /// Example: {"720p": 1.0, "1080p": 1.5, "4k": 2.5} - /// - public string? VideoResolutionMultipliers { get; set; } - - /// - /// Cost multiplier for batch processing operations, if applicable - /// - /// - /// This represents a cost reduction factor for batch API usage. - /// Example: 0.5 means 50% discount (half price), 0.6 means 40% discount. - /// Applied to the standard token costs when requests are processed through batch APIs. - /// - public decimal? BatchProcessingMultiplier { get; set; } - - /// - /// Indicates if this model supports batch processing - /// - /// - /// When true, requests can be processed through batch endpoints with the BatchProcessingMultiplier discount applied. - /// - public bool SupportsBatchProcessing { get; set; } - - /// - /// Quality-based cost multipliers for image generation as JSON string - /// - /// - /// JSON object containing quality-to-multiplier mappings. - /// Example: {"standard": 1.0, "hd": 2.0} - /// - public string? ImageQualityMultipliers { get; set; } - - /// - /// Resolution-based cost multipliers for image generation as JSON string - /// - /// - /// JSON object containing resolution-to-multiplier mappings. - /// Example: {"1024x1024": 1.0, "1792x1024": 1.5, "1024x1792": 1.5} - /// - public string? ImageResolutionMultipliers { get; set; } - - /// - /// Cost per million cached input tokens for prompt caching in USD, if applicable - /// - /// - /// This represents the cost for processing one million cached input tokens (reading from cache). - /// Used by providers like Anthropic Claude and Google Gemini that offer prompt caching. - /// Typically much lower than standard input token costs (e.g., 10% of regular cost). - /// - public decimal? CachedInputCostPerMillionTokens { get; set; } - - /// - /// Cost per million tokens for writing to the prompt cache in USD, if applicable - /// - /// - /// This represents the cost for writing one million tokens to the prompt cache. - /// Used by providers like Anthropic Claude and Google Gemini that offer prompt caching. - /// The write cost is incurred when new content is added to the cache. - /// - public decimal? CachedInputWriteCostPerMillionTokens { get; set; } - - /// - /// Cost per search unit for reranking models in USD per 1000 units, if applicable - /// - /// - /// Used by reranking models like Cohere Rerank that charge per search unit rather than per token. - /// A search unit typically consists of 1 query + up to 100 documents to be ranked. - /// Documents over 500 tokens are split into chunks, each counting as a separate document. - /// - public decimal? CostPerSearchUnit { get; set; } - - /// - /// Cost per inference step for image generation models in USD, if applicable - /// - /// - /// Used by providers like Fireworks that charge based on the number of iterative refinement steps. - /// Different models require different numbers of steps to generate an image. - /// Example: FLUX.1[schnell] uses 4 steps × $0.00035/step = $0.0014 per image. - /// Example: SDXL typically uses 30 steps × $0.00013/step = $0.0039 per image. - /// - public decimal? CostPerInferenceStep { get; set; } - - /// - /// Default number of inference steps for this model - /// - /// - /// Indicates the standard number of iterative refinement steps this model uses for image generation. - /// Used when the client request doesn't specify a custom step count. - /// Example: FLUX.1[schnell] uses 4 steps for fast generation, SDXL uses 30 steps for higher quality. - /// - public int? DefaultInferenceSteps { get; set; } - } -} diff --git a/ConduitLLM.Configuration/DTOs/ModelCostExportDto.cs b/ConduitLLM.Configuration/DTOs/ModelCostExportDto.cs deleted file mode 100644 index a9fa66a3c..000000000 --- a/ConduitLLM.Configuration/DTOs/ModelCostExportDto.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace ConduitLLM.Configuration.DTOs -{ - /// - /// DTO for exporting model costs - /// - public class ModelCostExportDto - { - public string CostName { get; set; } = string.Empty; - public PricingModel PricingModel { get; set; } = PricingModel.Standard; - public string? PricingConfiguration { get; set; } - public decimal InputCostPerMillionTokens { get; set; } - 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; } - public decimal? BatchProcessingMultiplier { get; set; } - public bool SupportsBatchProcessing { get; set; } - public decimal? CostPerSearchUnit { get; set; } - public decimal? CostPerInferenceStep { get; set; } - public int? DefaultInferenceSteps { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Configuration/DTOs/ModelProviderMappingDto.cs b/ConduitLLM.Configuration/DTOs/ModelProviderMappingDto.cs deleted file mode 100644 index 25fb615d1..000000000 --- a/ConduitLLM.Configuration/DTOs/ModelProviderMappingDto.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Configuration.DTOs -{ - /// - /// Data transfer object for model-provider mappings - /// - public class ModelProviderMappingDto - { - /// - /// Unique identifier for the mapping - /// - public int Id { get; set; } - - /// - /// The model alias used in client requests - /// - [Required(ErrorMessage = "Model Alias is required")] - public string ModelAlias { get; set; } = string.Empty; - - /// - /// The ID of the canonical Model entity - /// - [Required(ErrorMessage = "Model ID is required")] - public int ModelId { get; set; } - - /// - /// The provider-specific model identifier - /// - [Required(ErrorMessage = "Provider Model ID is required")] - public string ProviderModelId { get; set; } = string.Empty; - - /// - /// The ID of the provider - /// - [Required(ErrorMessage = "Provider ID is required")] - public int ProviderId { get; set; } - - /// - /// Provider reference information (populated when retrieving mappings) - /// - public ProviderReferenceDto? Provider { get; set; } - - /// - /// The priority of this mapping (lower values have higher priority) - /// - public int Priority { get; set; } - - /// - /// Whether this mapping is currently enabled - /// - public bool IsEnabled { get; set; } = true; - - /// - /// Provider-specific override for maximum context tokens. - /// If null, uses Model.Capabilities.MaxTokens. - /// - public int? MaxContextTokensOverride { get; set; } - - - /// - /// Provider variation of the model (e.g., "Q4_K_M", "GGUF", "4bit-128g", "instruct") - /// - public string? ProviderVariation { get; set; } - - /// - /// Quality score of the provider's model version. - /// 1.0 = identical to original, 0.95 = 5% quality loss, etc. - /// - public decimal? QualityScore { get; set; } - - /// - /// Whether this model is the default for its capability type - /// - public bool IsDefault { get; set; } = false; - - /// - /// The capability type this model is default for (if IsDefault is true) - /// - public string? DefaultCapabilityType { get; set; } - - /// - /// Date when the mapping was created - /// - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - - /// - /// Date when the mapping was last updated - /// - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - - /// - /// Optional notes or description for this mapping - /// - public string? Notes { get; set; } - } -} diff --git a/ConduitLLM.Configuration/Data/ConfigurationDbContext.cs b/ConduitLLM.Configuration/Data/ConfigurationDbContext.cs deleted file mode 100644 index fc84b64db..000000000 --- a/ConduitLLM.Configuration/Data/ConfigurationDbContext.cs +++ /dev/null @@ -1,552 +0,0 @@ -using ConduitLLM.Configuration.Data; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.EntityConfigurations; -using ModelProviderMappingEntity = ConduitLLM.Configuration.Entities.ModelProviderMapping; - -using Microsoft.EntityFrameworkCore; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Configuration -{ - /// - /// Database context for ConduitLLM configuration - /// - public class ConduitDbContext : DbContext, IConfigurationDbContext - { - /// - /// Initializes a new instance of the ConfigurationDbContext - /// - /// The options to be used by the context - public ConduitDbContext(DbContextOptions options) : base(options) - { - } - - /// - /// Database set for virtual keys - /// - public virtual DbSet VirtualKeys { get; set; } = null!; - - /// - /// Database set for virtual key groups - /// - public virtual DbSet VirtualKeyGroups { get; set; } = null!; - - /// - /// Database set for virtual key group transactions - /// - public virtual DbSet VirtualKeyGroupTransactions { get; set; } = null!; - - /// - /// Database set for request logs - /// - public virtual DbSet RequestLogs { get; set; } = null!; - - /// - /// Database set for billing audit events - /// - public virtual DbSet BillingAuditEvents { get; set; } = null!; - - /// - /// Database set for virtual key spend history - /// - public virtual DbSet VirtualKeySpendHistory { get; set; } = null!; - - /// - /// Database set for virtual key spend history (alias for backward compatibility) - /// - public virtual DbSet VirtualKeySpendHistories => VirtualKeySpendHistory; - - - /// - /// Database set for notifications - /// - public virtual DbSet Notifications { get; set; } = null!; - - /// - /// Database set for global settings - /// - public virtual DbSet GlobalSettings { get; set; } = null!; - - /// - /// Database set for model costs - /// - public virtual DbSet ModelCosts { get; set; } = null!; - - /// - /// Database set for model provider mappings - /// - public virtual DbSet ModelProviderMappings { get; set; } = null!; - - /// - /// Database set for models - /// - public virtual DbSet Models { get; set; } = null!; - - /// - /// Database set for model series - /// - public virtual DbSet ModelSeries { get; set; } = null!; - - /// - /// Database set for model capabilities - /// - public virtual DbSet ModelCapabilities { get; set; } = null!; - - /// - /// Database set for model authors - /// - public virtual DbSet ModelAuthors { get; set; } = null!; - - /// - /// Database set for model identifiers - /// - public virtual DbSet ModelIdentifiers { get; set; } = null!; - - /// - /// Database set for media records - /// - public virtual DbSet MediaRecords { get; set; } = null!; - - /// - /// Database set for media retention policies - /// - public virtual DbSet MediaRetentionPolicies { get; set; } = null!; - - /// - /// Database set for providers - /// - public virtual DbSet Providers { get; set; } = null!; - - /// - /// Database set for provider key credentials - /// - 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 - /// - public virtual DbSet ModelCostMappings { get; set; } = null!; - - /// - /// Database set for async tasks - /// - public virtual DbSet AsyncTasks { get; set; } = null!; - - // MediaLifecycleRecords removed - consolidated into MediaRecords table - // Migration: 20250827194408_ConsolidateMediaTables.cs - - /// - /// Database set for batch operation history - /// - public virtual DbSet BatchOperationHistory { get; set; } = null!; - - /// - /// Database set for cache configurations - /// - public virtual DbSet CacheConfigurations { get; set; } = null!; - - /// - /// Database set for cache configuration audit logs - /// - public virtual DbSet CacheConfigurationAudits { get; set; } = null!; - - public bool IsTestEnvironment { get; set; } = false; - - /// - /// Configures the model for the context - /// - /// The model builder - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - // Configure VirtualKeyGroup entity - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.HasIndex(e => e.ExternalGroupId); - - // Configure relationships - entity.HasMany(e => e.VirtualKeys) - .WithOne(e => e.VirtualKeyGroup) - .HasForeignKey(e => e.VirtualKeyGroupId) - .OnDelete(DeleteBehavior.Restrict); - - entity.HasMany(e => e.Transactions) - .WithOne(e => e.VirtualKeyGroup) - .HasForeignKey(e => e.VirtualKeyGroupId) - .OnDelete(DeleteBehavior.Cascade); - }); - - // Configure VirtualKey entity - modelBuilder.Entity(entity => - { - entity.HasIndex(e => e.KeyHash).IsUnique(); - - // Configure navigation property for RequestLogs - entity.HasMany(e => e.RequestLogs) - .WithOne(e => e.VirtualKey) - .HasForeignKey(e => e.VirtualKeyId) - .OnDelete(DeleteBehavior.Cascade); - - // Configure navigation property for SpendHistory - entity.HasMany(e => e.SpendHistory) - .WithOne(e => e.VirtualKey) - .HasForeignKey(e => e.VirtualKeyId) - .OnDelete(DeleteBehavior.Cascade); - - // Configure navigation property for Notifications - entity.HasMany(e => e.Notifications) - .WithOne(e => e.VirtualKey) - .HasForeignKey(e => e.VirtualKeyId) - .OnDelete(DeleteBehavior.Cascade); - }); - - // Configure GlobalSetting entity - modelBuilder.Entity(entity => - { - entity.HasIndex(e => e.Key).IsUnique(); - }); - - // Configure RequestLog entity - modelBuilder.Entity(entity => - { - entity.HasOne(e => e.VirtualKey) - .WithMany(e => e.RequestLogs) - .HasForeignKey(e => e.VirtualKeyId) - .OnDelete(DeleteBehavior.Restrict); - }); - - // Configure ModelCost entity - modelBuilder.Entity(entity => - { - entity.HasIndex(e => e.CostName); - - // Configure many-to-many relationship through ModelCostMapping - entity.HasMany(e => e.ModelCostMappings) - .WithOne(e => e.ModelCost) - .HasForeignKey(e => e.ModelCostId) - .OnDelete(DeleteBehavior.Cascade); - }); - - // Configure ModelCostMapping entity (junction table) - modelBuilder.Entity(entity => - { - entity.HasIndex(e => new { e.ModelCostId, e.ModelProviderMappingId }) - .IsUnique(); // Each model-cost combination should be unique - - entity.HasOne(e => e.ModelCost) - .WithMany(e => e.ModelCostMappings) - .HasForeignKey(e => e.ModelCostId) - .OnDelete(DeleteBehavior.Cascade); - - entity.HasOne(e => e.ModelProviderMapping) - .WithMany(e => e.ModelCostMappings) - .HasForeignKey(e => e.ModelProviderMappingId) - .OnDelete(DeleteBehavior.Cascade); - }); - - // Configure VirtualKeySpendHistory entity - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - // Remove redundant relationship configuration as it's already defined by annotations and the VirtualKey configuration - }); - - // Configure Notification entity - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - // 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 => - { - entity.HasKey(e => e.Id); - // Create a non-unique index on the filter type and IP address/CIDR fields - entity.HasIndex(e => new { e.FilterType, e.IpAddressOrCidr }); - // Create an index for IsEnabled to quickly filter active rules - 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 => - { - entity.HasKey(e => e.Id); - entity.HasIndex(e => e.VirtualKeyId); - entity.HasIndex(e => e.Type); - entity.HasIndex(e => e.State); - entity.HasIndex(e => e.CreatedAt); - entity.HasIndex(e => e.IsArchived); - entity.HasIndex(e => new { e.VirtualKeyId, e.CreatedAt }); - - // Composite index for archival queries - entity.HasIndex(e => new { e.IsArchived, e.CompletedAt, e.State }) - .HasDatabaseName("IX_AsyncTasks_Archival"); - - // Index for cleanup queries - entity.HasIndex(e => new { e.IsArchived, e.ArchivedAt }) - .HasDatabaseName("IX_AsyncTasks_Cleanup"); - - entity.HasOne(e => e.VirtualKey) - .WithMany() - .HasForeignKey(e => e.VirtualKeyId) - .OnDelete(DeleteBehavior.Cascade); - - // Configure large text fields without specifying provider-specific types - // EF Core will map these to appropriate text types for each provider - // By not specifying MaxLength, EF Core treats these as unlimited length text - entity.Property(e => e.Payload); - entity.Property(e => e.Result); - entity.Property(e => e.Error); - entity.Property(e => e.Metadata); - }); - - // Configure MediaRecord entity - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.HasIndex(e => e.StorageKey).IsUnique(); - entity.HasIndex(e => e.VirtualKeyId); - entity.HasIndex(e => e.ExpiresAt); - entity.HasIndex(e => e.CreatedAt); - entity.HasIndex(e => new { e.VirtualKeyId, e.CreatedAt }); - - entity.HasOne(e => e.VirtualKey) - .WithMany() - .HasForeignKey(e => e.VirtualKeyId) - .OnDelete(DeleteBehavior.Cascade); - }); - - // MediaLifecycleRecord configuration removed - consolidated into MediaRecords - // Migration: 20250827194408_ConsolidateMediaTables.cs - - // Configure MediaRetentionPolicy entity - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.HasIndex(e => e.Name).IsUnique(); - entity.HasIndex(e => e.IsDefault); - entity.HasIndex(e => e.IsActive); - - // Ensure only one default policy - entity.HasIndex(e => e.IsDefault) - .HasFilter("\"IsDefault\" = true") - .IsUnique(); - }); - - // Configure VirtualKeyGroup relationship with MediaRetentionPolicy - modelBuilder.Entity(entity => - { - entity.HasOne(e => e.MediaRetentionPolicy) - .WithMany(p => p.VirtualKeyGroups) - .HasForeignKey(e => e.MediaRetentionPolicyId) - .OnDelete(DeleteBehavior.SetNull); - }); - - // Configure BatchOperationHistory entity - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.OperationId); - entity.HasIndex(e => e.VirtualKeyId); - entity.HasIndex(e => e.OperationType); - entity.HasIndex(e => e.Status); - entity.HasIndex(e => e.StartedAt); - entity.HasIndex(e => new { e.VirtualKeyId, e.StartedAt }); - entity.HasIndex(e => new { e.OperationType, e.Status, e.StartedAt }); - - entity.HasOne(e => e.VirtualKey) - .WithMany() - .HasForeignKey(e => e.VirtualKeyId) - .OnDelete(DeleteBehavior.Cascade); - }); - - // Configure CacheConfiguration entity - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - - // Apply filtered index only for non-test environments (PostgreSQL) - if (!IsTestEnvironment) - { - entity.HasIndex(e => e.Region).IsUnique().HasFilter("\"IsActive\" = true"); - } - else - { - // For SQLite in tests, use a regular unique index - entity.HasIndex(e => e.Region).IsUnique(); - } - - entity.HasIndex(e => new { e.Region, e.IsActive }); - entity.HasIndex(e => e.UpdatedAt); - entity.Property(e => e.Version).IsConcurrencyToken(); - }); - - // Configure CacheConfigurationAudit entity - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.HasIndex(e => e.Region); - entity.HasIndex(e => e.ChangedAt); - entity.HasIndex(e => new { e.Region, e.ChangedAt }); - entity.HasIndex(e => e.ChangedBy); - }); - - // Configure VirtualKeyGroupTransaction entity - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.HasIndex(e => e.VirtualKeyGroupId); - entity.HasIndex(e => e.CreatedAt); - entity.HasIndex(e => new { e.VirtualKeyGroupId, e.CreatedAt }); - entity.HasIndex(e => new { e.IsDeleted, e.CreatedAt }); - entity.HasIndex(e => e.ReferenceType); - entity.HasIndex(e => e.TransactionType); - - // Store enums as integers - entity.Property(e => e.TransactionType) - .HasConversion(); - - entity.Property(e => e.ReferenceType) - .HasConversion(); - }); - - modelBuilder.ApplyConfigurationEntityConfigurations(IsTestEnvironment); - - // Apply Model entity configurations with all indexes and relationships - modelBuilder.ApplyModelConfigurations(); - - // Apply BillingAuditEvent configuration - modelBuilder.ApplyConfiguration(new EntityConfigurations.BillingAuditEventConfiguration()); - - // Note: ModelProviderMapping and Provider are now included in test environments - // as they are required by the application code during tests - } - } -} diff --git a/ConduitLLM.Configuration/Data/MigrationExtensions.cs b/ConduitLLM.Configuration/Data/MigrationExtensions.cs deleted file mode 100644 index c31122084..000000000 --- a/ConduitLLM.Configuration/Data/MigrationExtensions.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Configuration.Data -{ - /// - /// Extension methods for database migration - /// - public static class MigrationExtensions - { - /// - /// Add migration services to DI container - /// - public static IServiceCollection AddDatabaseMigration(this IServiceCollection services) - { - services.AddScoped(); - return services; - } - - /// - /// Run database migrations during startup - /// - public static async Task RunDatabaseMigrationAsync(this IHost app) - { - var skipDatabaseInit = Environment.GetEnvironmentVariable("CONDUIT_SKIP_DATABASE_INIT")?.ToUpperInvariant() == "TRUE"; - - using var scope = app.Services.CreateScope(); - var logger = scope.ServiceProvider.GetRequiredService>(); - - if (skipDatabaseInit) - { - logger.LogWarning("CONDUIT_SKIP_DATABASE_INIT is set. Skipping database migrations."); - logger.LogWarning("Ensure your database schema is up to date!"); - return; - } - - var migrationService = scope.ServiceProvider.GetRequiredService(); - - try - { - logger.LogInformation("Running database migrations..."); - - var success = await migrationService.MigrateAsync(); - - if (!success) - { - throw new InvalidOperationException("Database migration failed. Check logs for details."); - } - - logger.LogInformation("Database migrations completed successfully"); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to run database migrations"); - throw new InvalidOperationException("Database migration failed. Application cannot start.", ex); - } - } - } -} \ No newline at end of file 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/IpFilterEntity.cs b/ConduitLLM.Configuration/Entities/IpFilterEntity.cs deleted file mode 100644 index 597ade214..000000000 --- a/ConduitLLM.Configuration/Entities/IpFilterEntity.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Configuration.Entities; - -/// -/// Represents an IP address or subnet filter used for API access control -/// -public class IpFilterEntity -{ - /// - /// Unique identifier for the IP filter - /// - [Key] - public int Id { get; set; } - - /// - /// Type of the IP filter (whitelist or blacklist) - /// - [Required] - [MaxLength(10)] - public string FilterType { get; set; } = "blacklist"; - - /// - /// The IP address or subnet in CIDR notation (e.g., "192.168.1.1" or "192.168.1.0/24") - /// - [Required] - [MaxLength(50)] - public string IpAddressOrCidr { get; set; } = string.Empty; - - /// - /// Optional description of the filter - /// - [MaxLength(500)] - public string? Description { get; set; } - - /// - /// Whether the filter is currently active - /// - public bool IsEnabled { get; set; } = true; - - /// - /// Date when the filter was created - /// - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - - /// - /// Date when the filter was last updated - /// - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - - /// - /// Concurrency token for optimistic concurrency control - /// - [Timestamp] - public byte[]? RowVersion { get; set; } -} diff --git a/ConduitLLM.Configuration/Entities/Model.cs b/ConduitLLM.Configuration/Entities/Model.cs deleted file mode 100644 index e3f75fd43..000000000 --- a/ConduitLLM.Configuration/Entities/Model.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace ConduitLLM.Configuration.Entities -{ - /// - /// Entity representing a machine learning model. Each Model can typically be found on one or more providers. - /// This is a convenient way to associate costs, capabilities, and configurations with a specific model. - /// We are assuming that the cost is primarily determined by the model variant and its associated provider. - /// - public class Model - { - [Key] - public int Id { get; set; } - - /// - /// The name of the model (e.g., GPT-4, Claude, etc.) - /// - [Required] - public string Name { get; set; } = string.Empty; - - /// - /// The version of the model (e.g., "v1", "v2", etc.) - /// - public string? Version { get; set; } = string.Empty; - - /// - /// A brief description of the model - /// - public string? Description { get; set; } = string.Empty; - - /// - /// The URL to the model's card (e.g., documentation, specifications, etc.) - /// - public string? ModelCardUrl { get; set; } = string.Empty; - - /// - /// Foreign key for the model series this model belongs to. - /// - public int ModelSeriesId { get; set; } - - /// - /// Navigation property to the model series. - /// - [ForeignKey("ModelSeriesId")] - public ModelSeries Series { get; set; } = new ModelSeries(); - - /// - /// Foreign key for the shared model capabilities. - /// - public int ModelCapabilitiesId { get; set; } - - /// - /// Navigation property to the model capabilities. - /// Multiple models can share the same capabilities instance. - /// - [ForeignKey("ModelCapabilitiesId")] - public ModelCapabilities Capabilities { get; set; } = new ModelCapabilities(); - - /// - /// Navigation property for all identifiers associated with this model. - /// - public virtual ICollection Identifiers { get; set; } = new List(); - - /// - /// Navigation property for all provider mappings using this model. - /// - public virtual ICollection ProviderMappings { get; set; } = new List(); - - /// - /// Whether the model is active and available for use. - /// - public bool IsActive { get; set; } = true; - - /// - /// Internal storage for model-specific parameters. - /// When null, Parameters property will fall back to ModelSeries.Parameters. - /// - [Column("Parameters")] - public string? ModelParameters { get; set; } - - /// - /// JSON string containing parameter definitions for UI generation. - /// If ModelParameters is null, falls back to ModelSeries.Parameters. - /// Allows model-specific parameter overrides while inheriting series defaults. - /// - [NotMapped] - public string? Parameters - { - get => ModelParameters ?? Series?.Parameters ?? "{}"; - set => ModelParameters = value; - } - - /// - /// Date the model was created. - /// - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - - /// - /// Date the model was last updated. - /// - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - } - -} \ No newline at end of file diff --git a/ConduitLLM.Configuration/Entities/ModelCapabilities.cs b/ConduitLLM.Configuration/Entities/ModelCapabilities.cs deleted file mode 100644 index 1f87e03e3..000000000 --- a/ConduitLLM.Configuration/Entities/ModelCapabilities.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Configuration.Entities -{ - public class ModelCapabilities - { - [Key] - public int Id { get; set; } - - /// - /// The maximum tokens that can be generated for this capability. - /// - public int MaxTokens { get; set; } = 4096; - - /// - /// The minimum tokens that can be generated for this capability. - /// - public int MinTokens { get; set; } = 1; - - /// - /// Indicates whether this model supports vision/image inputs. - /// - 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. - /// - public bool SupportsImageGeneration { get; set; } = false; - - /// - /// Indicates whether this model supports video generation. - /// - public bool SupportsVideoGeneration { get; set; } = false; - - /// - /// Indicates whether this model supports embedding generation. - /// - public bool SupportsEmbeddings { get; set; } = false; - - /// - /// Indicates whether this model supports chat completions. - /// - public bool SupportsChat { get; set; } = false; - - /// - /// Indicates whether this model supports function calling. - /// - public bool SupportsFunctionCalling { get; set; } = false; - - /// - /// Indicates whether this model supports streaming responses. - /// - public bool SupportsStreaming { get; set; } = false; - - /// - /// The tokenizer type used by this model (e.g., "cl100k_base", "p50k_base", "claude"). - /// - 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 deleted file mode 100644 index ad26e876d..000000000 --- a/ConduitLLM.Configuration/Entities/ModelCost.cs +++ /dev/null @@ -1,370 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace ConduitLLM.Configuration.Entities; - -/// -/// Represents cost configuration that can be applied to multiple models in the system. -/// This entity stores pricing information for different operations (input/output tokens, embeddings, images). -/// -/// -/// ModelCost entities are linked to specific ModelProviderMapping records through the ModelCostMappings collection. -/// This allows one cost configuration to be applied to multiple models (e.g., same cost for Llama across different providers). -/// The pricing information is used to calculate costs for each request processed through the system, -/// enabling detailed cost reporting and budget management. -/// -public class ModelCost -{ - /// - /// Gets or sets the unique identifier for the model cost entry. - /// - [Key] - public int Id { get; set; } - - /// - /// Gets or sets a user-friendly name for this cost configuration. - /// - /// - /// Examples: "GPT-4 Standard Pricing", "Llama 3 Unified Cost", "Embedding Models - Ada" - /// This helps administrators identify and manage different cost configurations. - /// - [Required] - [MaxLength(255)] - public string CostName { get; set; } = string.Empty; - - /// - /// Gets or sets the pricing model type that determines how costs are calculated. - /// - /// - /// This field determines which pricing strategy to use for cost calculation. - /// Different providers use different pricing models (per-token, per-video, per-step, etc.). - /// Defaults to Standard (token-based) for backward compatibility. - /// - [Required] - public PricingModel PricingModel { get; set; } = PricingModel.Standard; - - /// - /// Gets or sets the JSON configuration for complex pricing models. - /// - /// - /// Contains model-specific pricing configuration based on PricingModel: - /// - PerVideo: {"rates": {"512p_6": 0.10, "720p_10": 0.15}} - /// - PerSecondVideo: {"baseRate": 0.09, "resolutionMultipliers": {"720p": 1.0}} - /// - InferenceSteps: {"costPerStep": 0.00013, "defaultSteps": 30} - /// - TieredTokens: {"tiers": [{"maxContext": 200000, "inputCost": 400}]} - /// Null for Standard pricing model which uses direct fields. - /// - [Column(TypeName = "text")] - public string? PricingConfiguration { get; set; } - - /// - /// Gets or sets the cost per million input tokens for chat/completion requests. - /// - /// - /// This represents the cost in USD for processing one million input tokens. - /// This format aligns with industry standard pricing from providers like Anthropic and OpenAI. - /// Example: 15.00 means $15 per million input tokens. - /// Stored with high precision (decimal 18,10) to accommodate fractional costs. - /// Used primarily when PricingModel is Standard. - /// - [Column(TypeName = "decimal(18, 10)")] - public decimal InputCostPerMillionTokens { get; set; } = 0; - - /// - /// Gets or sets the cost per million output tokens for chat/completion requests. - /// - /// - /// This represents the cost in USD for generating one million output tokens. - /// This format aligns with industry standard pricing from providers like Anthropic and OpenAI. - /// Example: 75.00 means $75 per million output tokens. - /// Stored with high precision (decimal 18,10) to accommodate fractional costs. - /// - [Column(TypeName = "decimal(18, 10)")] - public decimal OutputCostPerMillionTokens { get; set; } = 0; - - /// - /// Gets or sets the cost per million tokens for embedding requests, if applicable. - /// - /// - /// This represents the cost in USD for processing one million tokens in embedding requests. - /// This format aligns with industry standard pricing from providers like Anthropic and OpenAI. - /// Example: 0.10 means $0.10 per million embedding tokens. - /// Nullable because not all models support embedding operations. - /// Stored with high precision (decimal 18,10) to accommodate fractional costs. - /// - [Column(TypeName = "decimal(18, 10)")] - public decimal? EmbeddingCostPerMillionTokens { get; set; } - - /// - /// Gets or sets the cost per image for image generation requests, if applicable. - /// - /// - /// This represents the cost in USD for generating each image. - /// Nullable because not all models support image generation. - /// Stored with moderate precision (decimal 18,4) as image costs are typically higher than token costs. - /// - [Column(TypeName = "decimal(18, 4)")] - public decimal? ImageCostPerImage { get; set; } - - /// - /// Gets or sets the creation timestamp of this cost record. - /// - /// - /// Automatically set to UTC time when a new record is created. - /// - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - - /// - /// Gets or sets the last update timestamp of this cost record. - /// - /// - /// Should be updated whenever the cost record is modified. - /// - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - - /// - /// Gets or sets the model type for categorization. - /// - /// - /// Indicates the type of operations this model cost applies to. - /// Used for filtering and organizing costs in the UI. - /// - [Required] - [MaxLength(50)] - public string ModelType { get; set; } = "chat"; - - /// - /// Gets or sets whether this cost configuration is active. - /// - /// - /// When false, this cost configuration is ignored during cost calculations. - /// Allows disabling costs without deleting the configuration. - /// - public bool IsActive { get; set; } = true; - - /// - /// Gets or sets the effective date for this pricing. - /// - /// - /// The date from which this pricing becomes active. - /// Used for historical cost tracking and future price changes. - /// - public DateTime EffectiveDate { get; set; } = DateTime.UtcNow; - - /// - /// Gets or sets the expiry date for this pricing. - /// - /// - /// Optional date when this pricing expires. - /// Null means the pricing has no expiration date. - /// - public DateTime? ExpiryDate { get; set; } - - /// - /// Optional description for this model cost entry - /// - [MaxLength(500)] - public string? Description { get; set; } - - /// - /// Priority value for this model cost entry - /// - /// - /// Higher priority patterns are evaluated first when matching model names. - /// Default is 0. Use higher values for more specific patterns. - /// - 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. - /// - /// - /// This represents the base cost in USD for generating each second of video. - /// The actual cost may be adjusted based on resolution using VideoResolutionMultipliers. - /// Nullable because not all models support video generation. - /// Stored with moderate precision (decimal 18,4) for video generation costs. - /// - [Column(TypeName = "decimal(18, 4)")] - public decimal? VideoCostPerSecond { get; set; } - - /// - /// Gets or sets the resolution-based cost multipliers for video generation. - /// - /// - /// JSON object containing resolution-to-multiplier mappings. - /// Example: {"720p": 1.0, "1080p": 1.5, "4k": 2.5} - /// The base VideoCostPerSecond is multiplied by these values based on the requested resolution. - /// Stored as JSON text in the database. - /// - public string? VideoResolutionMultipliers { get; set; } - - /// - /// Gets or sets the cost multiplier for batch processing operations, if applicable. - /// - /// - /// This represents a cost reduction factor for batch API usage. - /// Example: 0.5 means 50% discount (half price), 0.6 means 40% discount. - /// Applied to the standard token costs when requests are processed through batch APIs. - /// Nullable because not all models support batch processing. - /// Stored with moderate precision (decimal 18,4) for percentage-based multipliers. - /// - [Column(TypeName = "decimal(18, 4)")] - public decimal? BatchProcessingMultiplier { get; set; } - - /// - /// Gets or sets whether this model supports batch processing. - /// - /// - /// Indicates if the model has batch API capabilities for discounted processing. - /// When true, requests can be processed through batch endpoints with the BatchProcessingMultiplier discount applied. - /// Default is false for backward compatibility. - /// - public bool SupportsBatchProcessing { get; set; } - - /// - /// Gets or sets the quality-based cost multipliers for image generation. - /// - /// - /// JSON object containing quality-to-multiplier mappings. - /// Example: {"standard": 1.0, "hd": 2.0} - /// The base ImageCostPerImage is multiplied by these values based on the requested quality. - /// Stored as JSON text in the database. - /// - public string? ImageQualityMultipliers { get; set; } - - /// - /// Gets or sets the resolution-based cost multipliers for image generation. - /// - /// - /// JSON object containing resolution-to-multiplier mappings. - /// Example: {"1024x1024": 1.0, "1792x1024": 1.5, "1024x1792": 1.5} - /// The base ImageCostPerImage is multiplied by these values based on the requested resolution. - /// Stored as JSON text in the database. - /// - public string? ImageResolutionMultipliers { get; set; } - - /// - /// Gets or sets the cost per million cached input tokens for prompt caching, if applicable. - /// - /// - /// This represents the cost in USD for processing one million cached input tokens (reading from cache). - /// Used by providers like Anthropic Claude and Google Gemini that offer prompt caching. - /// Typically much lower than standard input token costs (e.g., 10% of regular cost). - /// Example: 1.50 means $1.50 per million cached tokens. - /// Nullable because not all models support prompt caching. - /// Stored with high precision (decimal 18,10) to accommodate fractional costs. - /// - [Column(TypeName = "decimal(18, 10)")] - public decimal? CachedInputCostPerMillionTokens { get; set; } - - /// - /// Gets or sets the cost per million tokens for writing to the prompt cache, if applicable. - /// - /// - /// This represents the cost in USD for writing one million tokens to the prompt cache. - /// Used by providers like Anthropic Claude and Google Gemini that offer prompt caching. - /// Typically higher than cached read costs but may be lower than standard input costs. - /// The write cost is incurred when new content is added to the cache. - /// Example: 3.75 means $3.75 per million tokens written to cache. - /// Nullable because not all models support prompt caching. - /// Stored with high precision (decimal 18,10) to accommodate fractional costs. - /// - [Column(TypeName = "decimal(18, 10)")] - public decimal? CachedInputWriteCostPerMillionTokens { get; set; } - - /// - /// Gets or sets the cost per search unit for reranking models, if applicable. - /// - /// - /// This represents the cost in USD per 1000 search units. - /// Used by reranking models like Cohere Rerank that charge per search unit rather than per token. - /// A search unit typically consists of 1 query + up to 100 documents to be ranked. - /// Documents over 500 tokens are split into chunks, each counting as a separate document. - /// Nullable because not all models use search unit pricing. - /// Stored with moderate precision (decimal 18,8) to accommodate search unit costs. - /// - [Column(TypeName = "decimal(18, 8)")] - public decimal? CostPerSearchUnit { get; set; } - - /// - /// Gets or sets the cost per inference step for image generation models, if applicable. - /// - /// - /// This represents the cost in USD for each inference step during image generation. - /// Used by providers like Fireworks that charge based on the number of iterative refinement steps. - /// Different models require different numbers of steps to generate an image. - /// Example: FLUX.1[schnell] uses 4 steps × $0.00035/step = $0.0014 per image. - /// Example: SDXL typically uses 30 steps × $0.00013/step = $0.0039 per image. - /// Nullable because not all image models use step-based pricing. - /// Stored with moderate precision (decimal 18,8) to accommodate per-step costs. - /// - [Column(TypeName = "decimal(18, 8)")] - public decimal? CostPerInferenceStep { get; set; } - - /// - /// Gets or sets the default number of inference steps for this model. - /// - /// - /// Indicates the standard number of iterative refinement steps this model uses for image generation. - /// Used when the client request doesn't specify a custom step count. - /// Different models have different optimal step counts for quality vs speed tradeoffs. - /// Example: FLUX.1[schnell] uses 4 steps for fast generation, SDXL uses 30 steps for higher quality. - /// Nullable because not all models use step-based generation or have configurable steps. - /// - public int? DefaultInferenceSteps { get; set; } - - /// - /// Gets or sets the collection of model mappings that use this cost configuration. - /// - /// - /// This navigation property represents the many-to-many relationship between ModelCost and ModelProviderMapping. - /// Through this collection, one cost configuration can be applied to multiple models across different providers. - /// - public virtual ICollection ModelCostMappings { get; set; } = new List(); -} 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/ModelIdentifier.cs b/ConduitLLM.Configuration/Entities/ModelIdentifier.cs deleted file mode 100644 index 7b90f92eb..000000000 --- a/ConduitLLM.Configuration/Entities/ModelIdentifier.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace ConduitLLM.Configuration.Entities -{ - /// - /// Maps various model identifiers used by different providers to a canonical Model entity. - /// This allows us to recognize that "gpt-4-0125-preview", "gpt-4-turbo-preview", and "gpt-4-1106-preview" - /// all refer to the same underlying model. - /// - public class ModelIdentifier - { - /// - /// Unique identifier for this model identifier mapping. - /// - [Key] - public int Id { get; set; } - - /// - /// Foreign key to the canonical Model entity. - /// - public int ModelId { get; set; } - - /// - /// The identifier string used by a provider or in API calls. - /// Examples: "gpt-4-0125-preview", "claude-3-opus-20240229", "llama-3-70b-instruct" - /// - [Required] - [MaxLength(200)] - public string Identifier { get; set; } = string.Empty; - - /// - /// The provider or source that uses this identifier. - /// Examples: "openai", "azure", "anthropic", "openrouter", "deepinfra" - /// Null indicates a universal identifier. - /// - [MaxLength(100)] - public string? Provider { get; set; } - - /// - /// Indicates whether this is the primary/canonical identifier for the model. - /// - public bool IsPrimary { get; set; } = false; - - /// - /// Navigation property to the associated Model. - /// - [ForeignKey("ModelId")] - public virtual Model Model { get; set; } = null!; - - /// - /// Additional metadata about this identifier (e.g., deprecated status, release date). - /// Stored as JSON. - /// - public string? Metadata { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Configuration/Entities/ModelProviderMapping.cs b/ConduitLLM.Configuration/Entities/ModelProviderMapping.cs deleted file mode 100644 index 40b0d84f8..000000000 --- a/ConduitLLM.Configuration/Entities/ModelProviderMapping.cs +++ /dev/null @@ -1,263 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text.Json; - -namespace ConduitLLM.Configuration.Entities -{ - /// - /// Maps a generic model alias (e.g., "gpt-4-turbo") to a specific provider's model name - /// and associates it with provider credentials. This entity enables routing requests to - /// specific provider models regardless of the model name used in the request. - /// - public class ModelProviderMapping - { - /// - /// Unique identifier for the model-provider mapping. - /// - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } - - /// - /// User-friendly model alias used in client requests. - /// This is the name that clients will use in their API calls (e.g., "gpt-4"). - /// - [Required] - [MaxLength(100)] - public string ModelAlias { get; set; } = string.Empty; - - /// - /// The actual model identifier expected by the provider. - /// This is the provider-specific model name (e.g., "gpt-4-turbo-preview", "claude-3-opus-20240229"). - /// - [Required] - [MaxLength(100)] - public string ProviderModelId { get; set; } = string.Empty; - - /// - /// Foreign key to the provider entity. - /// Links this mapping to the provider used to authenticate with the provider. - /// - public int ProviderId { get; set; } - - /// - /// Navigation property to the associated provider. - /// Contains authentication details for connecting to the provider. - /// - [ForeignKey("ProviderId")] - public virtual Provider Provider { get; set; } = null!; - - /// - /// Indicates whether this mapping is currently active. - /// When false, the router will not use this mapping for routing requests. - /// - public bool IsEnabled { get; set; } = true; - - /// - /// Provider-specific override for maximum context tokens. - /// If null, uses Model.Capabilities.MaxTokens. - /// Some providers may have different limits than the base model. - /// - public int? MaxContextTokensOverride { get; set; } - - /// - /// Gets the effective maximum context tokens for this provider mapping. - /// - [NotMapped] - public int MaxContextTokens => MaxContextTokensOverride ?? Model?.Capabilities?.MaxTokens ?? 4096; - - /// - /// The UTC timestamp when this mapping was created. - /// - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - - /// - /// The UTC timestamp when this mapping was last updated. - /// - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - - /// - /// JSON object containing provider-specific capability overrides. - /// Use this when a provider has different capabilities than the base model. - /// Example: {"supportsFunctionCalling": false} if provider disabled this feature. - /// - public string? CapabilityOverrides { get; set; } - - // Helper properties that read from Model.Capabilities with optional overrides - - /// - /// Gets whether this model supports vision, checking overrides first. - /// - [NotMapped] - public bool SupportsVision => GetCapability(nameof(SupportsVision), - () => Model?.Capabilities?.SupportsVision ?? false); - - /// - /// Gets whether this model supports chat, checking overrides first. - /// - [NotMapped] - public bool SupportsChat => GetCapability(nameof(SupportsChat), - () => Model?.Capabilities?.SupportsChat ?? false); - - /// - /// Gets whether this model supports function calling, checking overrides first. - /// - [NotMapped] - public bool SupportsFunctionCalling => GetCapability(nameof(SupportsFunctionCalling), - () => Model?.Capabilities?.SupportsFunctionCalling ?? false); - - /// - /// Gets whether this model supports streaming, checking overrides first. - /// - [NotMapped] - 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. - /// - [NotMapped] - public bool SupportsImageGeneration => GetCapability(nameof(SupportsImageGeneration), - () => Model?.Capabilities?.SupportsImageGeneration ?? false); - - /// - /// Gets whether this model supports video generation, checking overrides first. - /// - [NotMapped] - public bool SupportsVideoGeneration => GetCapability(nameof(SupportsVideoGeneration), - () => Model?.Capabilities?.SupportsVideoGeneration ?? false); - - /// - /// Gets whether this model supports embeddings, checking overrides first. - /// - [NotMapped] - public bool SupportsEmbeddings => GetCapability(nameof(SupportsEmbeddings), - () => Model?.Capabilities?.SupportsEmbeddings ?? false); - - /// - /// Gets the tokenizer type from Model.Capabilities. - /// - [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. - /// - public bool IsDefault { get; set; } = false; - - /// - /// The capability type this model is default for (e.g., "chat", "transcription", "tts", "realtime"). - /// Only relevant when IsDefault is true. - /// - [MaxLength(50)] - public string? DefaultCapabilityType { get; set; } - - /// - /// Required foreign key to the associated Model entity. - /// Every provider mapping must reference a canonical model. - /// - [Required] - public int ModelId { get; set; } - - /// - /// Navigation property to the associated Model. - /// Contains metadata, capabilities, and configuration for the model. - /// - [ForeignKey("ModelId")] - public virtual Model Model { get; set; } = null!; - - - /// - /// Represents the variation of the model provided by the provider. - /// Examples: "Q4_K_M", "GGUF", "4bit-128g", "fine-tuned-medical", "instruct" - /// - public string? ProviderVariation { get; set; } // // "4-bit-quantized", "fine-tuned-v2" - - /// - /// Represents the quality score of the model provided by the provider. - /// 1.0 = identical to original - /// 0.95 = 5% quality loss (typical for good quantization) - /// 0.8 = 20% quality loss (aggressive quantization) - /// - - public decimal? QualityScore { get; set; } // Provider's quality vs original - - /// - /// Gets or sets the collection of cost configurations applied to this model mapping. - /// - /// - /// This navigation property represents the many-to-many relationship between ModelProviderMapping and ModelCost. - /// A model can have multiple cost configurations (e.g., different costs for different time periods or regions). - /// - public virtual ICollection ModelCostMappings { get; set; } = new List(); - - /// - /// Helper method to get capability value, checking overrides first. - /// - /// The name of the capability to check. - /// Function to get the default value from Model.Capabilities. - /// The capability value, considering overrides. - private bool GetCapability(string capabilityName, Func defaultValue) - { - if (string.IsNullOrEmpty(CapabilityOverrides)) - return defaultValue(); - - try - { - var overrides = JsonDocument.Parse(CapabilityOverrides); - var propertyName = char.ToLower(capabilityName[0]) + capabilityName.Substring(1); - - if (overrides.RootElement.TryGetProperty(propertyName, out var element) && - (element.ValueKind == JsonValueKind.True || element.ValueKind == JsonValueKind.False)) - { - return element.GetBoolean(); - } - } - catch - { - // Invalid JSON or parsing error, fall back to default - } - - return defaultValue(); - } - } -} 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/EntityConfigurations/ModelEntityConfiguration.cs b/ConduitLLM.Configuration/EntityConfigurations/ModelEntityConfiguration.cs deleted file mode 100644 index 832ac0c93..000000000 --- a/ConduitLLM.Configuration/EntityConfigurations/ModelEntityConfiguration.cs +++ /dev/null @@ -1,135 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Configuration.EntityConfigurations -{ - /// - /// Entity Framework configuration for Model-related entities. - /// Defines indexes and constraints for optimal query performance. - /// - public class ModelEntityConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) - { - // Index for querying models by series - builder.HasIndex(e => e.ModelSeriesId) - .HasDatabaseName("IX_Model_ModelSeriesId"); - - // Index for querying models by capabilities - builder.HasIndex(e => e.ModelCapabilitiesId) - .HasDatabaseName("IX_Model_ModelCapabilitiesId"); - } - } - - public class ModelIdentifierEntityConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) - { - // Unique constraint on provider + identifier combination - // This ensures no duplicate identifiers within the same provider - builder.HasIndex(e => new { e.Provider, e.Identifier }) - .IsUnique() - .HasDatabaseName("IX_ModelIdentifier_Provider_Identifier_Unique"); - - // Index for querying identifiers by model - builder.HasIndex(e => e.ModelId) - .HasDatabaseName("IX_ModelIdentifier_ModelId"); - - // Index for querying by identifier alone (for lookups) - builder.HasIndex(e => e.Identifier) - .HasDatabaseName("IX_ModelIdentifier_Identifier"); - - // Index for finding primary identifiers - builder.HasIndex(e => e.IsPrimary) - .HasDatabaseName("IX_ModelIdentifier_IsPrimary") - .HasFilter("\"IsPrimary\" = true"); // PostgreSQL syntax - } - } - - public class ModelSeriesEntityConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) - { - // Index for querying series by author - builder.HasIndex(e => e.AuthorId) - .HasDatabaseName("IX_ModelSeries_AuthorId"); - - // Index for querying series by tokenizer type - builder.HasIndex(e => e.TokenizerType) - .HasDatabaseName("IX_ModelSeries_TokenizerType"); - - // Unique constraint on series name within an author - builder.HasIndex(e => new { e.AuthorId, e.Name }) - .IsUnique() - .HasDatabaseName("IX_ModelSeries_AuthorId_Name_Unique"); - } - } - - public class ModelCapabilitiesEntityConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) - { - // Index for finding models with specific capabilities - builder.HasIndex(e => e.SupportsChat) - .HasDatabaseName("IX_ModelCapabilities_SupportsChat") - .HasFilter("\"SupportsChat\" = true"); - - builder.HasIndex(e => e.SupportsVision) - .HasDatabaseName("IX_ModelCapabilities_SupportsVision") - .HasFilter("\"SupportsVision\" = true"); - - builder.HasIndex(e => e.SupportsFunctionCalling) - .HasDatabaseName("IX_ModelCapabilities_SupportsFunctionCalling") - .HasFilter("\"SupportsFunctionCalling\" = true"); - - builder.HasIndex(e => e.SupportsImageGeneration) - .HasDatabaseName("IX_ModelCapabilities_SupportsImageGeneration") - .HasFilter("\"SupportsImageGeneration\" = true"); - - builder.HasIndex(e => e.SupportsVideoGeneration) - .HasDatabaseName("IX_ModelCapabilities_SupportsVideoGeneration") - .HasFilter("\"SupportsVideoGeneration\" = true"); - - // Composite index for common capability queries - builder.HasIndex(e => new { e.SupportsChat, e.SupportsFunctionCalling, e.SupportsStreaming }) - .HasDatabaseName("IX_ModelCapabilities_Chat_Function_Streaming"); - } - } - - public class ModelProviderMappingEntityConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) - { - // Index for querying mappings by model (now required, no filter needed) - builder.HasIndex(e => e.ModelId) - .HasDatabaseName("IX_ModelProviderMapping_ModelId"); - - // Index for finding enabled mappings - builder.HasIndex(e => new { e.ProviderId, e.IsEnabled }) - .HasDatabaseName("IX_ModelProviderMapping_ProviderId_IsEnabled") - .HasFilter("\"IsEnabled\" = true"); - - // Index for quality score queries (for finding best quality providers) - builder.HasIndex(e => new { e.ModelId, e.QualityScore }) - .HasDatabaseName("IX_ModelProviderMapping_ModelId_QualityScore") - .HasFilter("\"QualityScore\" IS NOT NULL"); - - // Index for capability overrides (to find mappings with custom capabilities) - builder.HasIndex(e => e.CapabilityOverrides) - .HasDatabaseName("IX_ModelProviderMapping_CapabilityOverrides") - .HasFilter("\"CapabilityOverrides\" IS NOT NULL"); - } - } - - public class ModelAuthorEntityConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) - { - // Unique constraint on author name - builder.HasIndex(e => e.Name) - .IsUnique() - .HasDatabaseName("IX_ModelAuthor_Name_Unique"); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Configuration/Extensions/CacheServiceExtensions.cs b/ConduitLLM.Configuration/Extensions/CacheServiceExtensions.cs deleted file mode 100644 index 492ec58a9..000000000 --- a/ConduitLLM.Configuration/Extensions/CacheServiceExtensions.cs +++ /dev/null @@ -1,194 +0,0 @@ -using ConduitLLM.Configuration.Interfaces; -using ConduitLLM.Configuration.Options; -using ConduitLLM.Configuration.Services; -using ConduitLLM.Configuration.Utilities; - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Configuration.Extensions -{ - /// - /// Extension methods for configuring cache services - /// - public static class CacheServiceExtensions - { - /// - /// Adds the cache service to the service collection with options from configuration - /// - /// The service collection - /// The configuration - /// The service collection for chaining - public static IServiceCollection AddCacheService(this IServiceCollection services, IConfiguration configuration) - { - // Register memory cache if not already registered - services.AddMemoryCache(); - - // Configure options from configuration and environment variables - services.Configure(options => - { - // Get values from configuration section - var section = configuration.GetSection(CacheOptions.SectionName); - - // Manually set properties from configuration - if (int.TryParse(section["DefaultAbsoluteExpirationMinutes"], out var absMinutes)) - { - options.DefaultAbsoluteExpirationMinutes = absMinutes; - } - - if (int.TryParse(section["DefaultSlidingExpirationMinutes"], out var slidingMinutes)) - { - options.DefaultSlidingExpirationMinutes = slidingMinutes; - } - - if (bool.TryParse(section["UseDefaultExpirationTimes"], out var useDefault)) - { - options.UseDefaultExpirationTimes = useDefault; - } - - if (bool.TryParse(section["IsEnabled"], out var isEnabled)) - { - options.IsEnabled = isEnabled; - } - - if (section["CacheType"] != null) - { - options.CacheType = section["CacheType"] ?? options.CacheType; - } - - if (int.TryParse(section["DefaultExpirationMinutes"], out var defaultExpMinutes)) - { - options.DefaultExpirationMinutes = defaultExpMinutes; - } - - if (int.TryParse(section["MaxCacheItems"], out var maxItems)) - { - options.MaxCacheItems = maxItems; - } - - if (section["RedisConnectionString"] != null) - { - options.RedisConnectionString = section["RedisConnectionString"] ?? options.RedisConnectionString; - } - - if (section["RedisInstanceName"] != null) - { - options.RedisInstanceName = section["RedisInstanceName"] ?? options.RedisInstanceName; - } - - if (bool.TryParse(section["IncludeModelInKey"], out var includeModel)) - { - options.IncludeModelInKey = includeModel; - } - - if (bool.TryParse(section["IncludeProviderInKey"], out var includeProvider)) - { - options.IncludeProviderInKey = includeProvider; - } - - if (bool.TryParse(section["IncludeApiKeyInKey"], out var includeApiKey)) - { - options.IncludeApiKeyInKey = includeApiKey; - } - - if (bool.TryParse(section["IncludeTemperatureInKey"], out var includeTemp)) - { - options.IncludeTemperatureInKey = includeTemp; - } - - if (bool.TryParse(section["IncludeMaxTokensInKey"], out var includeMaxTokens)) - { - options.IncludeMaxTokensInKey = includeMaxTokens; - } - - if (bool.TryParse(section["IncludeTopPInKey"], out var includeTopP)) - { - options.IncludeTopPInKey = includeTopP; - } - - if (section["HashAlgorithm"] != null) - { - options.HashAlgorithm = section["HashAlgorithm"] ?? options.HashAlgorithm; - } - - // Override with environment variables if present - var absoluteExpEnv = Environment.GetEnvironmentVariable("CONDUIT_CACHE_ABSOLUTE_EXPIRATION_MINUTES"); - if (!string.IsNullOrEmpty(absoluteExpEnv) && int.TryParse(absoluteExpEnv, out var absMinutesEnv)) - { - options.DefaultAbsoluteExpirationMinutes = absMinutesEnv; - } - - var slidingExpEnv = Environment.GetEnvironmentVariable("CONDUIT_CACHE_SLIDING_EXPIRATION_MINUTES"); - if (!string.IsNullOrEmpty(slidingExpEnv) && int.TryParse(slidingExpEnv, out var slideMinutesEnv)) - { - options.DefaultSlidingExpirationMinutes = slideMinutesEnv; - } - - var useDefaultExpEnv = Environment.GetEnvironmentVariable("CONDUIT_CACHE_USE_DEFAULT_EXPIRATION"); - if (!string.IsNullOrEmpty(useDefaultExpEnv) && bool.TryParse(useDefaultExpEnv, out var useDefaultEnv)) - { - options.UseDefaultExpirationTimes = useDefaultEnv; - } - - var enabledEnv = Environment.GetEnvironmentVariable("CONDUIT_CACHE_ENABLED"); - if (!string.IsNullOrEmpty(enabledEnv) && bool.TryParse(enabledEnv, out var enabledEnvValue)) - { - options.IsEnabled = enabledEnvValue; - } - - var cacheTypeEnv = Environment.GetEnvironmentVariable("CONDUIT_CACHE_TYPE"); - if (!string.IsNullOrEmpty(cacheTypeEnv)) - { - options.CacheType = cacheTypeEnv; - } - }); - - // Configure Redis connection string from environment variables - // Priority: REDIS_URL -> CONDUIT_REDIS_CONNECTION_STRING -> config - services.Configure(options => - { - var redisUrl = Environment.GetEnvironmentVariable("REDIS_URL"); - var legacyConnectionString = Environment.GetEnvironmentVariable("CONDUIT_REDIS_CONNECTION_STRING"); - - if (!string.IsNullOrEmpty(redisUrl)) - { - // Get logger for validation - var loggerFactory = services.BuildServiceProvider().GetService(); - var logger = loggerFactory?.CreateLogger("CacheConfiguration") ?? - Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; - - // Validate the Redis URL - var isValid = RedisUrlValidator.ValidateAndLog(redisUrl, logger, "CacheService"); - - if (isValid) - { - try - { - // Parse Redis URL format into connection string - options.RedisConnectionString = RedisUrlParser.ParseRedisUrl(redisUrl); - // IsEnabled and CacheType will be automatically set by computed properties - return; // Successfully parsed, don't need to check legacy - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to parse REDIS_URL, will fall back to legacy configuration"); - // Don't fail startup over this - fall through to legacy check - } - } - } - - // Fall back to legacy connection string if REDIS_URL not provided or failed - if (!string.IsNullOrEmpty(legacyConnectionString)) - { - options.RedisConnectionString = legacyConnectionString; - } - }); - - // Register the cache service implementation - services.AddSingleton(); - - return services; - } - } -} diff --git a/ConduitLLM.Configuration/Extensions/ProviderMappingExtensions.cs b/ConduitLLM.Configuration/Extensions/ProviderMappingExtensions.cs deleted file mode 100644 index ab0db46ab..000000000 --- a/ConduitLLM.Configuration/Extensions/ProviderMappingExtensions.cs +++ /dev/null @@ -1,82 +0,0 @@ -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Configuration.Extensions -{ - /// - /// Extension methods for converting between Provider-related entities and DTOs - /// - public static class ProviderMappingExtensions - { - /// - /// Converts a Provider entity to a ProviderReferenceDto - /// - public static ProviderReferenceDto ToReferenceDto(this Provider provider) - { - return new ProviderReferenceDto - { - Id = provider.Id, - ProviderType = provider.ProviderType, - DisplayName = provider.ProviderName, - IsEnabled = provider.IsEnabled - }; - } - - /// - /// Converts a ModelProviderMapping entity to a ModelProviderMappingDto - /// - public static ModelProviderMappingDto ToDto(this ModelProviderMapping mapping) - { - return new ModelProviderMappingDto - { - Id = mapping.Id, - ModelAlias = mapping.ModelAlias, - ModelId = mapping.ModelId, - ProviderModelId = mapping.ProviderModelId, - ProviderId = mapping.ProviderId, - Provider = mapping.Provider?.ToReferenceDto(), - Priority = 0, // Entity doesn't have Priority - IsEnabled = mapping.IsEnabled, - MaxContextTokensOverride = mapping.MaxContextTokensOverride, - ProviderVariation = mapping.ProviderVariation, - QualityScore = mapping.QualityScore, - IsDefault = mapping.IsDefault, - DefaultCapabilityType = mapping.DefaultCapabilityType, - CreatedAt = mapping.CreatedAt, - UpdatedAt = mapping.UpdatedAt, - Notes = null // Entity doesn't have Notes - }; - } - - /// - /// Updates a ModelProviderMapping entity from a ModelProviderMappingDto - /// - public static void UpdateFromDto(this ModelProviderMapping mapping, ModelProviderMappingDto dto) - { - mapping.ModelAlias = dto.ModelAlias; - mapping.ModelId = dto.ModelId; - mapping.ProviderModelId = dto.ProviderModelId; - mapping.ProviderId = dto.ProviderId; - mapping.IsEnabled = dto.IsEnabled; - mapping.MaxContextTokensOverride = dto.MaxContextTokensOverride; - mapping.ProviderVariation = dto.ProviderVariation; - mapping.QualityScore = dto.QualityScore; - mapping.IsDefault = dto.IsDefault; - mapping.DefaultCapabilityType = dto.DefaultCapabilityType; - mapping.UpdatedAt = System.DateTime.UtcNow; - // Note: Priority and Notes are DTO-only properties - } - - /// - /// Creates a new ModelProviderMapping entity from a ModelProviderMappingDto - /// - public static ModelProviderMapping ToEntity(this ModelProviderMappingDto dto) - { - var mapping = new ModelProviderMapping(); - mapping.UpdateFromDto(dto); - mapping.Id = 0; // Reset ID for new entities - mapping.CreatedAt = System.DateTime.UtcNow; - return mapping; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Configuration/Extensions/ServiceCollectionExtensions.cs b/ConduitLLM.Configuration/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index a066735cb..000000000 --- a/ConduitLLM.Configuration/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,160 +0,0 @@ -using ConduitLLM.Configuration.Data; -using ConduitLLM.Configuration.Interfaces; -using ConduitLLM.Configuration.Options; -using ConduitLLM.Configuration.Repositories; -using ConduitLLM.Configuration.Services; - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Configuration.Extensions -{ - /// - /// Extension methods for configuring repository services - /// - public static class ServiceCollectionExtensions - { - /// - /// Adds repository services to the service collection - /// - /// The service collection - /// The service collection for chaining - public static IServiceCollection AddRepositories(this IServiceCollection services) - { - // Register DbContext interface - services.AddScoped(provider => - provider.GetRequiredService()); - - // Register repositories - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - // Register validator - services.AddScoped(); - - // 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(); - - // Register media record repository - services.AddScoped(); - - // Register cache configuration service - services.AddScoped(); - - return services; - } - - /// - /// Adds caching services to the service collection - /// - /// The service collection - /// The application configuration - /// The service collection for chaining - public static IServiceCollection AddCachingServices(this IServiceCollection services, IConfiguration configuration) - { - // Register cache options - services.Configure(configuration.GetSection(CacheOptions.SectionName)); - - // Add memory cache - services.AddMemoryCache(options => - { - var cacheSection = configuration.GetSection(CacheOptions.SectionName); - options.SizeLimit = cacheSection.GetValue("MaxCacheItems"); - }); - - // Add Redis connection factory - services.AddSingleton(); - - // Register cache service factory - services.AddSingleton(); - - // Register the appropriate distributed cache provider based on configuration - var cacheType = configuration.GetSection(CacheOptions.SectionName) - .GetValue("CacheType")?.ToLowerInvariant(); - - if (cacheType == "redis") - { - var redisConnectionString = configuration.GetSection(CacheOptions.SectionName) - .GetValue("RedisConnectionString"); - - var redisInstanceName = configuration.GetSection(CacheOptions.SectionName) - .GetValue("RedisInstanceName") ?? "conduitllm-cache"; - - if (!string.IsNullOrEmpty(redisConnectionString)) - { - services.AddStackExchangeRedisCache(options => - { - options.Configuration = redisConnectionString; - options.InstanceName = redisInstanceName; - }); - } - else - { - // Fall back to memory cache if Redis connection string is not configured - services.AddDistributedMemoryCache(); - } - } - else - { - // Use memory cache if Redis is not specified - services.AddDistributedMemoryCache(); - } - - // Register the ICacheService as a singleton but with factory-based initialization - services.AddSingleton(serviceProvider => - { - var logger = serviceProvider.GetRequiredService>(); - logger.LogInformation("[CacheService] Creating cache service during service registration..."); - - var factory = serviceProvider.GetRequiredService(); - - // Create the appropriate cache service based on configuration - // For simplicity in the synchronous service provider context, we'll block on the async result here - var cacheService = factory.CreateCacheServiceAsync().GetAwaiter().GetResult(); - logger.LogInformation("[CacheService] Cache service created successfully"); - return cacheService; - }); - - return services; - } - - /// - /// Adds database initialization services to the service collection - /// - /// The service collection - /// The service collection for chaining - public static IServiceCollection AddDatabaseInitialization(this IServiceCollection services) - { - // Register the simple migration service - services.AddScoped(); - - return services; - } - - } -} 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/ICacheService.cs b/ConduitLLM.Configuration/Interfaces/ICacheService.cs deleted file mode 100644 index 352452c9e..000000000 --- a/ConduitLLM.Configuration/Interfaces/ICacheService.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace ConduitLLM.Configuration.Interfaces -{ - /// - /// Interface for caching services - /// - public interface ICacheService - { - /// - /// Gets a value from the cache - /// - /// Type of the cached value - /// Cache key - /// Cached value or default if not found - T? Get(string key); - - /// - /// Sets a value in the cache - /// - /// Type of the value to cache - /// Cache key - /// Value to cache - /// Absolute expiration time - /// Sliding expiration time - void Set(string key, T value, TimeSpan? absoluteExpiration = null, TimeSpan? slidingExpiration = null); - - /// - /// Removes a value from the cache - /// - /// Cache key - void Remove(string key); - - /// - /// Gets a value from the cache or creates it if it doesn't exist - /// - /// Type of the value - /// Cache key - /// Factory function to create the value if not in cache - /// Absolute expiration time - /// Sliding expiration time - /// Cached or newly created value - Task GetOrCreateAsync(string key, Func> factory, TimeSpan? absoluteExpiration = null, TimeSpan? slidingExpiration = null); - - /// - /// Removes all cache entries with keys that start with the specified prefix - /// - /// Key prefix - void RemoveByPrefix(string prefix); - } -} diff --git a/ConduitLLM.Configuration/Interfaces/IConfigurationDbContext.cs b/ConduitLLM.Configuration/Interfaces/IConfigurationDbContext.cs deleted file mode 100644 index ec1a7e640..000000000 --- a/ConduitLLM.Configuration/Interfaces/IConfigurationDbContext.cs +++ /dev/null @@ -1,136 +0,0 @@ -// Import the model provider mapping from the root namespace -using ConduitLLM.Configuration.Entities; -using ModelProviderMappingEntity = ConduitLLM.Configuration.Entities.ModelProviderMapping; - -using Microsoft.EntityFrameworkCore; - -namespace ConduitLLM.Configuration.Interfaces -{ - /// - /// Interface for the configuration database context - /// - public interface IConfigurationDbContext : IDisposable - { - /// - /// Database set for virtual keys - /// - DbSet VirtualKeys { get; } - - /// - /// Database set for virtual key groups - /// - DbSet VirtualKeyGroups { get; } - - /// - /// Database set for virtual key group transactions - /// - DbSet VirtualKeyGroupTransactions { get; } - - /// - /// Database set for request logs - /// - DbSet RequestLogs { get; } - - /// - /// Database set for virtual key spend history - /// - DbSet VirtualKeySpendHistory { get; } - - - /// - /// Database set for notifications - /// - DbSet Notifications { get; } - - /// - /// Database set for global settings - /// - DbSet GlobalSettings { get; } - - /// - /// Database set for model costs - /// - DbSet ModelCosts { get; } - - /// - /// Database set for model provider mappings - /// - DbSet ModelProviderMappings { get; } - - /// - /// Database set for media records - /// - DbSet MediaRecords { get; } - - /// - /// Database set for media retention policies - /// - DbSet MediaRetentionPolicies { get; } - - /// - /// Database set for providers - /// - DbSet Providers { get; } - - /// - /// Database set for provider key credentials - /// - 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; } - - - /// - /// Database set for IP filters - /// - 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 - /// - DbSet AsyncTasks { get; } - - /// - /// Flag indicating if this is a test environment - /// - bool IsTestEnvironment { get; set; } - - /// - /// Saves changes to the database - /// - Task SaveChangesAsync(CancellationToken cancellationToken = default); - } -} 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/20250818194726_InitialCreate.Designer.cs b/ConduitLLM.Configuration/Migrations/20250818194726_InitialCreate.Designer.cs deleted file mode 100644 index 94aaf2e07..000000000 --- a/ConduitLLM.Configuration/Migrations/20250818194726_InitialCreate.Designer.cs +++ /dev/null @@ -1,2182 +0,0 @@ -// -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("20250818194726_InitialCreate")] - partial class InitialCreate - { - /// - 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.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.MediaLifecycleRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileSizeBytes") - .HasColumnType("bigint"); - - b.Property("GeneratedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("GeneratedByModel") - .IsRequired() - .HasColumnType("text"); - - b.Property("GenerationPrompt") - .IsRequired() - .HasColumnType("text"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("MediaType") - .IsRequired() - .HasColumnType("text"); - - b.Property("MediaUrl") - .IsRequired() - .HasColumnType("text"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("StorageKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("VirtualKeyId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("StorageKey") - .IsUnique(); - - b.HasIndex("VirtualKeyId"); - - b.HasIndex("ExpiresAt", "IsDeleted"); - - b.HasIndex("VirtualKeyId", "IsDeleted"); - - b.ToTable("MediaLifecycleRecords"); - }); - - 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.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("ModelSeriesId") - .HasColumnType("integer"); - - b.Property("ModelType") - .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.HasIndex("ModelType") - .HasDatabaseName("IX_Model_ModelType"); - - b.HasIndex("ModelSeriesId", "ModelType") - .HasDatabaseName("IX_Model_ModelSeriesId_ModelType"); - - 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("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ExternalGroupId"); - - 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.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.MediaLifecycleRecord", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") - .WithMany() - .HasForeignKey("VirtualKeyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - 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.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.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.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/20250818194726_InitialCreate.cs b/ConduitLLM.Configuration/Migrations/20250818194726_InitialCreate.cs deleted file mode 100644 index 824afbee2..000000000 --- a/ConduitLLM.Configuration/Migrations/20250818194726_InitialCreate.cs +++ /dev/null @@ -1,1482 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace ConduitLLM.Configuration.Migrations -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "CacheConfigurationAudits", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Region = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Action = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - OldConfigJson = table.Column(type: "text", nullable: true), - NewConfigJson = table.Column(type: "text", nullable: true), - Reason = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - ChangedBy = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - ChangedAt = table.Column(type: "timestamp with time zone", nullable: false), - ChangeSource = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), - Success = table.Column(type: "boolean", nullable: false), - ErrorMessage = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_CacheConfigurationAudits", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "CacheConfigurations", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Region = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Enabled = table.Column(type: "boolean", nullable: false), - DefaultTtlSeconds = table.Column(type: "integer", nullable: true), - MaxTtlSeconds = table.Column(type: "integer", nullable: true), - MaxEntries = table.Column(type: "bigint", nullable: true), - MaxMemoryBytes = table.Column(type: "bigint", nullable: true), - EvictionPolicy = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - UseMemoryCache = table.Column(type: "boolean", nullable: false), - UseDistributedCache = table.Column(type: "boolean", nullable: false), - EnableCompression = table.Column(type: "boolean", nullable: false), - CompressionThresholdBytes = table.Column(type: "bigint", nullable: true), - Priority = table.Column(type: "integer", nullable: false), - EnableDetailedStats = table.Column(type: "boolean", nullable: false), - ExtendedConfig = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedBy = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - UpdatedBy = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - Version = table.Column(type: "bytea", rowVersion: true, nullable: true), - IsActive = table.Column(type: "boolean", nullable: false), - Notes = table.Column(type: "character varying(500)", maxLength: 500, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_CacheConfigurations", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "GlobalSettings", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Key = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Value = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false), - Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_GlobalSettings", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "IpFilters", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - FilterType = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), - IpAddressOrCidr = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - IsEnabled = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - RowVersion = table.Column(type: "bytea", rowVersion: true, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_IpFilters", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ModelAuthors", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - WebsiteUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ModelAuthors", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ModelCapabilities", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - MaxTokens = table.Column(type: "integer", nullable: false), - MinTokens = table.Column(type: "integer", nullable: false), - SupportsVision = table.Column(type: "boolean", nullable: false), - SupportsAudioTranscription = table.Column(type: "boolean", nullable: false), - SupportsTextToSpeech = table.Column(type: "boolean", nullable: false), - SupportsRealtimeAudio = table.Column(type: "boolean", nullable: false), - SupportsImageGeneration = table.Column(type: "boolean", nullable: false), - SupportsVideoGeneration = table.Column(type: "boolean", nullable: false), - SupportsEmbeddings = table.Column(type: "boolean", nullable: false), - SupportsChat = table.Column(type: "boolean", nullable: false), - SupportsFunctionCalling = table.Column(type: "boolean", nullable: false), - SupportsStreaming = table.Column(type: "boolean", nullable: false), - TokenizerType = table.Column(type: "integer", nullable: false), - SupportedVoices = table.Column(type: "text", nullable: true), - SupportedLanguages = table.Column(type: "text", nullable: true), - SupportedFormats = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ModelCapabilities", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ModelCosts", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - CostName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - PricingModel = table.Column(type: "integer", nullable: false), - PricingConfiguration = table.Column(type: "text", nullable: true), - InputCostPerMillionTokens = table.Column(type: "numeric(18,10)", nullable: false), - OutputCostPerMillionTokens = table.Column(type: "numeric(18,10)", nullable: false), - EmbeddingCostPerMillionTokens = table.Column(type: "numeric(18,10)", nullable: true), - ImageCostPerImage = table.Column(type: "numeric(18,4)", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ModelType = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - IsActive = table.Column(type: "boolean", nullable: false), - EffectiveDate = table.Column(type: "timestamp with time zone", nullable: false), - ExpiryDate = table.Column(type: "timestamp with time zone", nullable: true), - Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - Priority = table.Column(type: "integer", nullable: false), - AudioCostPerMinute = table.Column(type: "numeric(18,4)", nullable: true), - AudioCostPerKCharacters = table.Column(type: "numeric(18,4)", nullable: true), - AudioInputCostPerMinute = table.Column(type: "numeric(18,4)", nullable: true), - AudioOutputCostPerMinute = table.Column(type: "numeric(18,4)", nullable: true), - VideoCostPerSecond = table.Column(type: "numeric(18,4)", nullable: true), - VideoResolutionMultipliers = table.Column(type: "text", nullable: true), - BatchProcessingMultiplier = table.Column(type: "numeric(18,4)", nullable: true), - SupportsBatchProcessing = table.Column(type: "boolean", nullable: false), - ImageQualityMultipliers = table.Column(type: "text", nullable: true), - ImageResolutionMultipliers = table.Column(type: "text", nullable: true), - CachedInputCostPerMillionTokens = table.Column(type: "numeric(18,10)", nullable: true), - CachedInputWriteCostPerMillionTokens = table.Column(type: "numeric(18,10)", nullable: true), - CostPerSearchUnit = table.Column(type: "numeric(18,8)", nullable: true), - CostPerInferenceStep = table.Column(type: "numeric(18,8)", nullable: true), - DefaultInferenceSteps = table.Column(type: "integer", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ModelCosts", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Providers", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ProviderType = table.Column(type: "integer", nullable: false), - ProviderName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - BaseUrl = table.Column(type: "text", nullable: true), - IsEnabled = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Providers", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "RouterConfigEntity", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", 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_RouterConfigEntity", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "VirtualKeyGroups", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ExternalGroupId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - GroupName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Balance = table.Column(type: "numeric(19,8)", nullable: false), - LifetimeCreditsAdded = table.Column(type: "numeric(19,8)", nullable: false), - LifetimeSpent = table.Column(type: "numeric(19,8)", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - RowVersion = table.Column(type: "bytea", rowVersion: true, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_VirtualKeyGroups", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ModelSeries", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - AuthorId = table.Column(type: "integer", nullable: false), - Name = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: true), - TokenizerType = table.Column(type: "integer", nullable: false), - Parameters = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ModelSeries", x => x.Id); - table.ForeignKey( - name: "FK_ModelSeries_ModelAuthors_AuthorId", - column: x => x.AuthorId, - principalTable: "ModelAuthors", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AudioCosts", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ProviderId = table.Column(type: "integer", nullable: false), - OperationType = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Model = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - CostUnit = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - CostPerUnit = table.Column(type: "numeric(10,6)", nullable: false), - MinimumCharge = table.Column(type: "numeric(10,6)", nullable: true), - AdditionalFactors = table.Column(type: "text", nullable: true), - IsActive = table.Column(type: "boolean", nullable: false), - EffectiveFrom = table.Column(type: "timestamp with time zone", nullable: false), - EffectiveTo = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AudioCosts", x => x.Id); - table.ForeignKey( - name: "FK_AudioCosts_Providers_ProviderId", - column: x => x.ProviderId, - principalTable: "Providers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AudioProviderConfigs", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ProviderId = table.Column(type: "integer", nullable: false), - TranscriptionEnabled = table.Column(type: "boolean", nullable: false), - DefaultTranscriptionModel = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - TextToSpeechEnabled = table.Column(type: "boolean", nullable: false), - DefaultTTSModel = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - DefaultTTSVoice = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - RealtimeEnabled = table.Column(type: "boolean", nullable: false), - DefaultRealtimeModel = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - RealtimeEndpoint = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - CustomSettings = table.Column(type: "text", nullable: true), - RoutingPriority = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AudioProviderConfigs", x => x.Id); - table.ForeignKey( - name: "FK_AudioProviderConfigs_Providers_ProviderId", - column: x => x.ProviderId, - principalTable: "Providers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AudioUsageLogs", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - VirtualKey = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - ProviderId = table.Column(type: "integer", nullable: false), - OperationType = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Model = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - RequestId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - SessionId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - DurationSeconds = table.Column(type: "double precision", nullable: true), - CharacterCount = table.Column(type: "integer", nullable: true), - InputTokens = table.Column(type: "integer", nullable: true), - OutputTokens = table.Column(type: "integer", nullable: true), - Cost = table.Column(type: "numeric(10,6)", nullable: false), - Language = table.Column(type: "character varying(10)", maxLength: 10, nullable: true), - Voice = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - StatusCode = table.Column(type: "integer", nullable: true), - ErrorMessage = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - IpAddress = table.Column(type: "character varying(45)", maxLength: 45, nullable: true), - UserAgent = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - Metadata = table.Column(type: "text", nullable: true), - Timestamp = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AudioUsageLogs", x => x.Id); - table.ForeignKey( - name: "FK_AudioUsageLogs_Providers_ProviderId", - column: x => x.ProviderId, - principalTable: "Providers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ProviderKeyCredentials", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ProviderId = table.Column(type: "integer", nullable: false), - ProviderAccountGroup = table.Column(type: "smallint", nullable: false), - ApiKey = table.Column(type: "text", nullable: true), - BaseUrl = table.Column(type: "text", nullable: true), - Organization = table.Column(type: "text", nullable: true), - KeyName = table.Column(type: "text", nullable: true), - IsPrimary = table.Column(type: "boolean", nullable: false), - IsEnabled = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ProviderKeyCredentials", x => x.Id); - table.CheckConstraint("CK_ProviderKeyCredential_AccountGroupRange", "\"ProviderAccountGroup\" >= 0 AND \"ProviderAccountGroup\" <= 32"); - table.CheckConstraint("CK_ProviderKeyCredential_PrimaryMustBeEnabled", "\"IsPrimary\" = false OR \"IsEnabled\" = true"); - table.ForeignKey( - name: "FK_ProviderKeyCredentials_Providers_ProviderId", - column: x => x.ProviderId, - principalTable: "Providers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - 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), - 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), - Name = table.Column(type: "text", nullable: false), - IsActive = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_FallbackConfigurations", x => x.Id); - table.ForeignKey( - name: "FK_FallbackConfigurations_RouterConfigEntity_RouterConfigId", - column: x => x.RouterConfigId, - principalTable: "RouterConfigEntity", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - 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_RouterConfigEntity_RouterConfigId", - column: x => x.RouterConfigId, - principalTable: "RouterConfigEntity", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "VirtualKeyGroupTransactions", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - VirtualKeyGroupId = table.Column(type: "integer", nullable: false), - TransactionType = table.Column(type: "integer", nullable: false), - Amount = table.Column(type: "numeric(18,6)", nullable: false), - BalanceAfter = table.Column(type: "numeric(18,6)", nullable: false), - ReferenceType = table.Column(type: "integer", nullable: false), - ReferenceId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - InitiatedBy = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - InitiatedByUserId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - IsDeleted = table.Column(type: "boolean", nullable: false), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_VirtualKeyGroupTransactions", x => x.Id); - table.ForeignKey( - name: "FK_VirtualKeyGroupTransactions_VirtualKeyGroups_VirtualKeyGrou~", - column: x => x.VirtualKeyGroupId, - principalTable: "VirtualKeyGroups", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "VirtualKeys", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - KeyName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - KeyHash = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - IsEnabled = table.Column(type: "boolean", nullable: false), - VirtualKeyGroupId = table.Column(type: "integer", nullable: false), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - Metadata = table.Column(type: "text", nullable: true), - AllowedModels = table.Column(type: "text", nullable: true), - RateLimitRpm = table.Column(type: "integer", nullable: true), - RateLimitRpd = table.Column(type: "integer", nullable: true), - RowVersion = table.Column(type: "bytea", rowVersion: true, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_VirtualKeys", x => x.Id); - table.ForeignKey( - name: "FK_VirtualKeys_VirtualKeyGroups_VirtualKeyGroupId", - column: x => x.VirtualKeyGroupId, - principalTable: "VirtualKeyGroups", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "Models", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "text", nullable: false), - Version = table.Column(type: "text", nullable: true), - Description = table.Column(type: "text", nullable: true), - ModelCardUrl = table.Column(type: "text", nullable: true), - ModelType = table.Column(type: "integer", nullable: false), - ModelSeriesId = table.Column(type: "integer", nullable: false), - ModelCapabilitiesId = table.Column(type: "integer", nullable: false), - IsActive = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Models", x => x.Id); - table.ForeignKey( - name: "FK_Models_ModelCapabilities_ModelCapabilitiesId", - column: x => x.ModelCapabilitiesId, - principalTable: "ModelCapabilities", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Models_ModelSeries_ModelSeriesId", - column: x => x.ModelSeriesId, - principalTable: "ModelSeries", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "FallbackModelMappings", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - FallbackConfigurationId = table.Column(type: "uuid", nullable: false), - ModelDeploymentId = table.Column(type: "uuid", nullable: false), - Order = table.Column(type: "integer", nullable: false), - SourceModelName = table.Column(type: "text", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_FallbackModelMappings", x => x.Id); - table.ForeignKey( - name: "FK_FallbackModelMappings_FallbackConfigurations_FallbackConfig~", - column: x => x.FallbackConfigurationId, - principalTable: "FallbackConfigurations", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AsyncTasks", - columns: table => new - { - Id = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Type = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - State = table.Column(type: "integer", nullable: false), - Payload = table.Column(type: "text", nullable: true), - Progress = table.Column(type: "integer", nullable: false), - ProgressMessage = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - Result = table.Column(type: "text", nullable: true), - Error = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - CompletedAt = table.Column(type: "timestamp with time zone", nullable: true), - VirtualKeyId = table.Column(type: "integer", nullable: false), - Metadata = table.Column(type: "text", nullable: true), - IsArchived = table.Column(type: "boolean", nullable: false), - ArchivedAt = table.Column(type: "timestamp with time zone", nullable: true), - LeasedBy = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - LeaseExpiryTime = table.Column(type: "timestamp with time zone", nullable: true), - Version = table.Column(type: "integer", nullable: false), - RetryCount = table.Column(type: "integer", nullable: false), - MaxRetries = table.Column(type: "integer", nullable: false), - IsRetryable = table.Column(type: "boolean", nullable: false), - NextRetryAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AsyncTasks", x => x.Id); - table.ForeignKey( - name: "FK_AsyncTasks_VirtualKeys_VirtualKeyId", - column: x => x.VirtualKeyId, - principalTable: "VirtualKeys", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "BatchOperationHistory", - columns: table => new - { - OperationId = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - OperationType = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - VirtualKeyId = table.Column(type: "integer", nullable: false), - TotalItems = table.Column(type: "integer", nullable: false), - SuccessCount = table.Column(type: "integer", nullable: false), - FailedCount = table.Column(type: "integer", nullable: false), - Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - StartedAt = table.Column(type: "timestamp with time zone", nullable: false), - CompletedAt = table.Column(type: "timestamp with time zone", nullable: true), - DurationSeconds = table.Column(type: "double precision", nullable: true), - ItemsPerSecond = table.Column(type: "double precision", nullable: true), - ErrorMessage = table.Column(type: "text", nullable: true), - CancellationReason = table.Column(type: "text", nullable: true), - ErrorDetails = table.Column(type: "text", nullable: true), - ResultSummary = table.Column(type: "text", nullable: true), - Metadata = table.Column(type: "text", nullable: true), - CheckpointData = table.Column(type: "text", nullable: true), - CanResume = table.Column(type: "boolean", nullable: false), - LastProcessedIndex = table.Column(type: "integer", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_BatchOperationHistory", x => x.OperationId); - table.ForeignKey( - name: "FK_BatchOperationHistory_VirtualKeys_VirtualKeyId", - column: x => x.VirtualKeyId, - principalTable: "VirtualKeys", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "MediaLifecycleRecords", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - VirtualKeyId = table.Column(type: "integer", nullable: false), - MediaType = table.Column(type: "text", nullable: false), - MediaUrl = table.Column(type: "text", nullable: false), - StorageKey = table.Column(type: "text", nullable: false), - FileSizeBytes = table.Column(type: "bigint", nullable: false), - ContentType = table.Column(type: "text", nullable: false), - GeneratedByModel = table.Column(type: "text", nullable: false), - GenerationPrompt = table.Column(type: "text", nullable: false), - GeneratedAt = table.Column(type: "timestamp with time zone", nullable: false), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), - Metadata = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - IsDeleted = table.Column(type: "boolean", nullable: false), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_MediaLifecycleRecords", x => x.Id); - table.ForeignKey( - name: "FK_MediaLifecycleRecords_VirtualKeys_VirtualKeyId", - column: x => x.VirtualKeyId, - principalTable: "VirtualKeys", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "MediaRecords", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StorageKey = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - VirtualKeyId = table.Column(type: "integer", nullable: false), - MediaType = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - ContentType = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - SizeBytes = table.Column(type: "bigint", nullable: true), - ContentHash = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - Provider = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), - Model = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - Prompt = table.Column(type: "text", nullable: true), - StorageUrl = table.Column(type: "text", nullable: true), - PublicUrl = table.Column(type: "text", nullable: true), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - LastAccessedAt = table.Column(type: "timestamp with time zone", nullable: true), - AccessCount = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_MediaRecords", x => x.Id); - table.ForeignKey( - name: "FK_MediaRecords_VirtualKeys_VirtualKeyId", - column: x => x.VirtualKeyId, - principalTable: "VirtualKeys", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Notifications", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - VirtualKeyId = table.Column(type: "integer", nullable: true), - Type = table.Column(type: "integer", nullable: false), - Severity = table.Column(type: "integer", nullable: false), - Message = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - IsRead = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Notifications", x => x.Id); - table.ForeignKey( - name: "FK_Notifications_VirtualKeys_VirtualKeyId", - column: x => x.VirtualKeyId, - principalTable: "VirtualKeys", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "RequestLogs", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - VirtualKeyId = table.Column(type: "integer", nullable: false), - ModelName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - RequestType = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - InputTokens = table.Column(type: "integer", nullable: false), - OutputTokens = table.Column(type: "integer", nullable: false), - Cost = table.Column(type: "numeric(10,6)", nullable: false), - ResponseTimeMs = table.Column(type: "double precision", nullable: false), - Timestamp = table.Column(type: "timestamp with time zone", nullable: false), - UserId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - ClientIp = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), - RequestPath = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - StatusCode = table.Column(type: "integer", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_RequestLogs", x => x.Id); - table.ForeignKey( - name: "FK_RequestLogs_VirtualKeys_VirtualKeyId", - column: x => x.VirtualKeyId, - principalTable: "VirtualKeys", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "VirtualKeySpendHistory", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - VirtualKeyId = table.Column(type: "integer", nullable: false), - Amount = table.Column(type: "numeric(10,6)", nullable: false), - Date = table.Column(type: "timestamp with time zone", nullable: false), - Timestamp = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_VirtualKeySpendHistory", x => x.Id); - table.ForeignKey( - name: "FK_VirtualKeySpendHistory_VirtualKeys_VirtualKeyId", - column: x => x.VirtualKeyId, - principalTable: "VirtualKeys", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ModelIdentifiers", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ModelId = table.Column(type: "integer", nullable: false), - Identifier = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Provider = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - IsPrimary = table.Column(type: "boolean", nullable: false), - Metadata = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ModelIdentifiers", x => x.Id); - table.ForeignKey( - name: "FK_ModelIdentifiers_Models_ModelId", - column: x => x.ModelId, - principalTable: "Models", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ModelProviderMappings", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ModelAlias = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - ProviderModelId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - ProviderId = table.Column(type: "integer", nullable: false), - IsEnabled = table.Column(type: "boolean", nullable: false), - MaxContextTokensOverride = table.Column(type: "integer", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - CapabilityOverrides = table.Column(type: "text", nullable: true), - IsDefault = table.Column(type: "boolean", nullable: false), - DefaultCapabilityType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), - ModelId = table.Column(type: "integer", nullable: false), - ProviderVariation = table.Column(type: "text", nullable: true), - QualityScore = table.Column(type: "numeric", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ModelProviderMappings", x => x.Id); - table.ForeignKey( - name: "FK_ModelProviderMappings_Models_ModelId", - column: x => x.ModelId, - principalTable: "Models", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_ModelProviderMappings_Providers_ProviderId", - column: x => x.ProviderId, - principalTable: "Providers", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "ModelCostMappings", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ModelCostId = table.Column(type: "integer", nullable: false), - ModelProviderMappingId = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - IsActive = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ModelCostMappings", x => x.Id); - table.ForeignKey( - name: "FK_ModelCostMappings_ModelCosts_ModelCostId", - column: x => x.ModelCostId, - principalTable: "ModelCosts", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ModelCostMappings_ModelProviderMappings_ModelProviderMappin~", - column: x => x.ModelProviderMappingId, - principalTable: "ModelProviderMappings", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_AsyncTasks_Archival", - table: "AsyncTasks", - columns: new[] { "IsArchived", "CompletedAt", "State" }); - - migrationBuilder.CreateIndex( - name: "IX_AsyncTasks_Cleanup", - table: "AsyncTasks", - columns: new[] { "IsArchived", "ArchivedAt" }); - - migrationBuilder.CreateIndex( - name: "IX_AsyncTasks_CreatedAt", - table: "AsyncTasks", - column: "CreatedAt"); - - migrationBuilder.CreateIndex( - name: "IX_AsyncTasks_IsArchived", - table: "AsyncTasks", - column: "IsArchived"); - - migrationBuilder.CreateIndex( - name: "IX_AsyncTasks_State", - table: "AsyncTasks", - column: "State"); - - migrationBuilder.CreateIndex( - name: "IX_AsyncTasks_Type", - table: "AsyncTasks", - column: "Type"); - - migrationBuilder.CreateIndex( - name: "IX_AsyncTasks_VirtualKeyId", - table: "AsyncTasks", - column: "VirtualKeyId"); - - migrationBuilder.CreateIndex( - name: "IX_AsyncTasks_VirtualKeyId_CreatedAt", - table: "AsyncTasks", - columns: new[] { "VirtualKeyId", "CreatedAt" }); - - migrationBuilder.CreateIndex( - name: "IX_AudioCosts_EffectiveFrom_EffectiveTo", - table: "AudioCosts", - columns: new[] { "EffectiveFrom", "EffectiveTo" }); - - migrationBuilder.CreateIndex( - name: "IX_AudioCosts_ProviderId_OperationType_Model_IsActive", - table: "AudioCosts", - columns: new[] { "ProviderId", "OperationType", "Model", "IsActive" }); - - migrationBuilder.CreateIndex( - name: "IX_AudioProviderConfigs_ProviderId", - table: "AudioProviderConfigs", - column: "ProviderId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_AudioUsageLogs_ProviderId_OperationType", - table: "AudioUsageLogs", - columns: new[] { "ProviderId", "OperationType" }); - - migrationBuilder.CreateIndex( - name: "IX_AudioUsageLogs_SessionId", - table: "AudioUsageLogs", - column: "SessionId"); - - migrationBuilder.CreateIndex( - name: "IX_AudioUsageLogs_Timestamp", - table: "AudioUsageLogs", - column: "Timestamp"); - - migrationBuilder.CreateIndex( - name: "IX_AudioUsageLogs_VirtualKey", - table: "AudioUsageLogs", - column: "VirtualKey"); - - migrationBuilder.CreateIndex( - name: "IX_BatchOperationHistory_OperationType", - table: "BatchOperationHistory", - column: "OperationType"); - - migrationBuilder.CreateIndex( - name: "IX_BatchOperationHistory_OperationType_Status_StartedAt", - table: "BatchOperationHistory", - columns: new[] { "OperationType", "Status", "StartedAt" }); - - migrationBuilder.CreateIndex( - name: "IX_BatchOperationHistory_StartedAt", - table: "BatchOperationHistory", - column: "StartedAt"); - - migrationBuilder.CreateIndex( - name: "IX_BatchOperationHistory_Status", - table: "BatchOperationHistory", - column: "Status"); - - migrationBuilder.CreateIndex( - name: "IX_BatchOperationHistory_VirtualKeyId", - table: "BatchOperationHistory", - column: "VirtualKeyId"); - - migrationBuilder.CreateIndex( - name: "IX_BatchOperationHistory_VirtualKeyId_StartedAt", - table: "BatchOperationHistory", - columns: new[] { "VirtualKeyId", "StartedAt" }); - - migrationBuilder.CreateIndex( - name: "IX_CacheConfigurationAudits_ChangedAt", - table: "CacheConfigurationAudits", - column: "ChangedAt"); - - migrationBuilder.CreateIndex( - name: "IX_CacheConfigurationAudits_ChangedBy", - table: "CacheConfigurationAudits", - column: "ChangedBy"); - - migrationBuilder.CreateIndex( - name: "IX_CacheConfigurationAudits_Region", - table: "CacheConfigurationAudits", - column: "Region"); - - migrationBuilder.CreateIndex( - name: "IX_CacheConfigurationAudits_Region_ChangedAt", - table: "CacheConfigurationAudits", - columns: new[] { "Region", "ChangedAt" }); - - migrationBuilder.CreateIndex( - name: "IX_CacheConfigurations_Region", - table: "CacheConfigurations", - column: "Region", - unique: true, - filter: "\"IsActive\" = true"); - - migrationBuilder.CreateIndex( - name: "IX_CacheConfigurations_Region_IsActive", - table: "CacheConfigurations", - columns: new[] { "Region", "IsActive" }); - - migrationBuilder.CreateIndex( - name: "IX_CacheConfigurations_UpdatedAt", - table: "CacheConfigurations", - column: "UpdatedAt"); - - 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_ModelDeployme~", - table: "FallbackModelMappings", - columns: new[] { "FallbackConfigurationId", "ModelDeploymentId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_FallbackModelMappings_FallbackConfigurationId_Order", - table: "FallbackModelMappings", - columns: new[] { "FallbackConfigurationId", "Order" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_GlobalSettings_Key", - table: "GlobalSettings", - column: "Key", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_IpFilters_FilterType_IpAddressOrCidr", - table: "IpFilters", - columns: new[] { "FilterType", "IpAddressOrCidr" }); - - migrationBuilder.CreateIndex( - name: "IX_IpFilters_IsEnabled", - table: "IpFilters", - column: "IsEnabled"); - - migrationBuilder.CreateIndex( - name: "IX_MediaLifecycleRecords_CreatedAt", - table: "MediaLifecycleRecords", - column: "CreatedAt"); - - migrationBuilder.CreateIndex( - name: "IX_MediaLifecycleRecords_ExpiresAt", - table: "MediaLifecycleRecords", - column: "ExpiresAt"); - - migrationBuilder.CreateIndex( - name: "IX_MediaLifecycleRecords_ExpiresAt_IsDeleted", - table: "MediaLifecycleRecords", - columns: new[] { "ExpiresAt", "IsDeleted" }); - - migrationBuilder.CreateIndex( - name: "IX_MediaLifecycleRecords_StorageKey", - table: "MediaLifecycleRecords", - column: "StorageKey", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_MediaLifecycleRecords_VirtualKeyId", - table: "MediaLifecycleRecords", - column: "VirtualKeyId"); - - migrationBuilder.CreateIndex( - name: "IX_MediaLifecycleRecords_VirtualKeyId_IsDeleted", - table: "MediaLifecycleRecords", - columns: new[] { "VirtualKeyId", "IsDeleted" }); - - migrationBuilder.CreateIndex( - name: "IX_MediaRecords_CreatedAt", - table: "MediaRecords", - column: "CreatedAt"); - - migrationBuilder.CreateIndex( - name: "IX_MediaRecords_ExpiresAt", - table: "MediaRecords", - column: "ExpiresAt"); - - migrationBuilder.CreateIndex( - name: "IX_MediaRecords_StorageKey", - table: "MediaRecords", - column: "StorageKey", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_MediaRecords_VirtualKeyId", - table: "MediaRecords", - column: "VirtualKeyId"); - - migrationBuilder.CreateIndex( - name: "IX_MediaRecords_VirtualKeyId_CreatedAt", - table: "MediaRecords", - columns: new[] { "VirtualKeyId", "CreatedAt" }); - - migrationBuilder.CreateIndex( - name: "IX_ModelAuthor_Name_Unique", - table: "ModelAuthors", - column: "Name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ModelCapabilities_Chat_Function_Streaming", - table: "ModelCapabilities", - columns: new[] { "SupportsChat", "SupportsFunctionCalling", "SupportsStreaming" }); - - migrationBuilder.CreateIndex( - name: "IX_ModelCapabilities_SupportsChat", - table: "ModelCapabilities", - column: "SupportsChat", - filter: "\"SupportsChat\" = true"); - - migrationBuilder.CreateIndex( - name: "IX_ModelCapabilities_SupportsFunctionCalling", - table: "ModelCapabilities", - column: "SupportsFunctionCalling", - filter: "\"SupportsFunctionCalling\" = true"); - - migrationBuilder.CreateIndex( - name: "IX_ModelCapabilities_SupportsImageGeneration", - table: "ModelCapabilities", - column: "SupportsImageGeneration", - filter: "\"SupportsImageGeneration\" = true"); - - migrationBuilder.CreateIndex( - name: "IX_ModelCapabilities_SupportsVideoGeneration", - table: "ModelCapabilities", - column: "SupportsVideoGeneration", - filter: "\"SupportsVideoGeneration\" = true"); - - migrationBuilder.CreateIndex( - name: "IX_ModelCapabilities_SupportsVision", - table: "ModelCapabilities", - column: "SupportsVision", - filter: "\"SupportsVision\" = true"); - - migrationBuilder.CreateIndex( - name: "IX_ModelCostMappings_ModelCostId_ModelProviderMappingId", - table: "ModelCostMappings", - columns: new[] { "ModelCostId", "ModelProviderMappingId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ModelCostMappings_ModelProviderMappingId", - table: "ModelCostMappings", - column: "ModelProviderMappingId"); - - migrationBuilder.CreateIndex( - name: "IX_ModelCosts_CostName", - table: "ModelCosts", - column: "CostName"); - - 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_ModelName", - table: "ModelDeployments", - column: "ModelName"); - - migrationBuilder.CreateIndex( - name: "IX_ModelDeployments_ProviderId", - table: "ModelDeployments", - column: "ProviderId"); - - migrationBuilder.CreateIndex( - name: "IX_ModelDeployments_RouterConfigId", - table: "ModelDeployments", - column: "RouterConfigId"); - - migrationBuilder.CreateIndex( - name: "IX_ModelIdentifier_Identifier", - table: "ModelIdentifiers", - column: "Identifier"); - - migrationBuilder.CreateIndex( - name: "IX_ModelIdentifier_IsPrimary", - table: "ModelIdentifiers", - column: "IsPrimary", - filter: "\"IsPrimary\" = true"); - - migrationBuilder.CreateIndex( - name: "IX_ModelIdentifier_ModelId", - table: "ModelIdentifiers", - column: "ModelId"); - - migrationBuilder.CreateIndex( - name: "IX_ModelIdentifier_Provider_Identifier_Unique", - table: "ModelIdentifiers", - columns: new[] { "Provider", "Identifier" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ModelProviderMapping_CapabilityOverrides", - table: "ModelProviderMappings", - column: "CapabilityOverrides", - filter: "\"CapabilityOverrides\" IS NOT NULL"); - - migrationBuilder.CreateIndex( - name: "IX_ModelProviderMapping_ModelId", - table: "ModelProviderMappings", - column: "ModelId"); - - migrationBuilder.CreateIndex( - name: "IX_ModelProviderMapping_ModelId_QualityScore", - table: "ModelProviderMappings", - columns: new[] { "ModelId", "QualityScore" }, - filter: "\"QualityScore\" IS NOT NULL"); - - migrationBuilder.CreateIndex( - name: "IX_ModelProviderMapping_ProviderId_IsEnabled", - table: "ModelProviderMappings", - columns: new[] { "ProviderId", "IsEnabled" }, - filter: "\"IsEnabled\" = true"); - - migrationBuilder.CreateIndex( - name: "IX_ModelProviderMappings_ModelAlias_ProviderId", - table: "ModelProviderMappings", - columns: new[] { "ModelAlias", "ProviderId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Model_ModelCapabilitiesId", - table: "Models", - column: "ModelCapabilitiesId"); - - migrationBuilder.CreateIndex( - name: "IX_Model_ModelSeriesId", - table: "Models", - column: "ModelSeriesId"); - - migrationBuilder.CreateIndex( - name: "IX_Model_ModelSeriesId_ModelType", - table: "Models", - columns: new[] { "ModelSeriesId", "ModelType" }); - - migrationBuilder.CreateIndex( - name: "IX_Model_ModelType", - table: "Models", - column: "ModelType"); - - migrationBuilder.CreateIndex( - name: "IX_ModelSeries_AuthorId", - table: "ModelSeries", - column: "AuthorId"); - - migrationBuilder.CreateIndex( - name: "IX_ModelSeries_AuthorId_Name_Unique", - table: "ModelSeries", - columns: new[] { "AuthorId", "Name" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ModelSeries_TokenizerType", - table: "ModelSeries", - column: "TokenizerType"); - - migrationBuilder.CreateIndex( - name: "IX_Notifications_VirtualKeyId", - table: "Notifications", - column: "VirtualKeyId"); - - migrationBuilder.CreateIndex( - name: "IX_ProviderKeyCredential_OnePrimaryPerProvider", - table: "ProviderKeyCredentials", - columns: new[] { "ProviderId", "IsPrimary" }, - unique: true, - filter: "\"IsPrimary\" = true"); - - migrationBuilder.CreateIndex( - name: "IX_ProviderKeyCredential_ProviderId", - table: "ProviderKeyCredentials", - column: "ProviderId"); - - migrationBuilder.CreateIndex( - name: "IX_ProviderKeyCredential_UniqueApiKeyPerProvider", - table: "ProviderKeyCredentials", - columns: new[] { "ProviderId", "ApiKey" }, - unique: true, - filter: "\"ApiKey\" IS NOT NULL"); - - migrationBuilder.CreateIndex( - name: "IX_Providers_ProviderType", - table: "Providers", - column: "ProviderType"); - - migrationBuilder.CreateIndex( - name: "IX_RequestLogs_VirtualKeyId", - table: "RequestLogs", - column: "VirtualKeyId"); - - migrationBuilder.CreateIndex( - name: "IX_RouterConfigEntity_LastUpdated", - table: "RouterConfigEntity", - column: "LastUpdated"); - - migrationBuilder.CreateIndex( - name: "IX_VirtualKeyGroups_ExternalGroupId", - table: "VirtualKeyGroups", - column: "ExternalGroupId"); - - migrationBuilder.CreateIndex( - name: "IX_VirtualKeyGroupTransactions_CreatedAt", - table: "VirtualKeyGroupTransactions", - column: "CreatedAt"); - - migrationBuilder.CreateIndex( - name: "IX_VirtualKeyGroupTransactions_IsDeleted_CreatedAt", - table: "VirtualKeyGroupTransactions", - columns: new[] { "IsDeleted", "CreatedAt" }); - - migrationBuilder.CreateIndex( - name: "IX_VirtualKeyGroupTransactions_ReferenceType", - table: "VirtualKeyGroupTransactions", - column: "ReferenceType"); - - migrationBuilder.CreateIndex( - name: "IX_VirtualKeyGroupTransactions_TransactionType", - table: "VirtualKeyGroupTransactions", - column: "TransactionType"); - - migrationBuilder.CreateIndex( - name: "IX_VirtualKeyGroupTransactions_VirtualKeyGroupId", - table: "VirtualKeyGroupTransactions", - column: "VirtualKeyGroupId"); - - migrationBuilder.CreateIndex( - name: "IX_VirtualKeyGroupTransactions_VirtualKeyGroupId_CreatedAt", - table: "VirtualKeyGroupTransactions", - columns: new[] { "VirtualKeyGroupId", "CreatedAt" }); - - migrationBuilder.CreateIndex( - name: "IX_VirtualKeys_KeyHash", - table: "VirtualKeys", - column: "KeyHash", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_VirtualKeys_VirtualKeyGroupId", - table: "VirtualKeys", - column: "VirtualKeyGroupId"); - - migrationBuilder.CreateIndex( - name: "IX_VirtualKeySpendHistory_VirtualKeyId", - table: "VirtualKeySpendHistory", - column: "VirtualKeyId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AsyncTasks"); - - migrationBuilder.DropTable( - name: "AudioCosts"); - - migrationBuilder.DropTable( - name: "AudioProviderConfigs"); - - migrationBuilder.DropTable( - name: "AudioUsageLogs"); - - migrationBuilder.DropTable( - name: "BatchOperationHistory"); - - migrationBuilder.DropTable( - name: "CacheConfigurationAudits"); - - migrationBuilder.DropTable( - name: "CacheConfigurations"); - - migrationBuilder.DropTable( - name: "FallbackModelMappings"); - - migrationBuilder.DropTable( - name: "GlobalSettings"); - - migrationBuilder.DropTable( - name: "IpFilters"); - - migrationBuilder.DropTable( - name: "MediaLifecycleRecords"); - - migrationBuilder.DropTable( - name: "MediaRecords"); - - migrationBuilder.DropTable( - name: "ModelCostMappings"); - - migrationBuilder.DropTable( - name: "ModelDeployments"); - - migrationBuilder.DropTable( - name: "ModelIdentifiers"); - - migrationBuilder.DropTable( - name: "Notifications"); - - migrationBuilder.DropTable( - name: "ProviderKeyCredentials"); - - migrationBuilder.DropTable( - name: "RequestLogs"); - - migrationBuilder.DropTable( - name: "VirtualKeyGroupTransactions"); - - migrationBuilder.DropTable( - name: "VirtualKeySpendHistory"); - - migrationBuilder.DropTable( - name: "FallbackConfigurations"); - - migrationBuilder.DropTable( - name: "ModelCosts"); - - migrationBuilder.DropTable( - name: "ModelProviderMappings"); - - migrationBuilder.DropTable( - name: "VirtualKeys"); - - migrationBuilder.DropTable( - name: "RouterConfigEntity"); - - migrationBuilder.DropTable( - name: "Models"); - - migrationBuilder.DropTable( - name: "Providers"); - - migrationBuilder.DropTable( - name: "VirtualKeyGroups"); - - migrationBuilder.DropTable( - name: "ModelCapabilities"); - - migrationBuilder.DropTable( - name: "ModelSeries"); - - migrationBuilder.DropTable( - name: "ModelAuthors"); - } - } -} diff --git a/ConduitLLM.Configuration/Migrations/20250819013218_SeedModelData.Designer.cs b/ConduitLLM.Configuration/Migrations/20250819013218_SeedModelData.Designer.cs deleted file mode 100644 index d4b96da41..000000000 --- a/ConduitLLM.Configuration/Migrations/20250819013218_SeedModelData.Designer.cs +++ /dev/null @@ -1,2182 +0,0 @@ -// -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("20250819013218_SeedModelData")] - partial class SeedModelData - { - /// - 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.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.MediaLifecycleRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileSizeBytes") - .HasColumnType("bigint"); - - b.Property("GeneratedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("GeneratedByModel") - .IsRequired() - .HasColumnType("text"); - - b.Property("GenerationPrompt") - .IsRequired() - .HasColumnType("text"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("MediaType") - .IsRequired() - .HasColumnType("text"); - - b.Property("MediaUrl") - .IsRequired() - .HasColumnType("text"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("StorageKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("VirtualKeyId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("StorageKey") - .IsUnique(); - - b.HasIndex("VirtualKeyId"); - - b.HasIndex("ExpiresAt", "IsDeleted"); - - b.HasIndex("VirtualKeyId", "IsDeleted"); - - b.ToTable("MediaLifecycleRecords"); - }); - - 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.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("ModelSeriesId") - .HasColumnType("integer"); - - b.Property("ModelType") - .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.HasIndex("ModelType") - .HasDatabaseName("IX_Model_ModelType"); - - b.HasIndex("ModelSeriesId", "ModelType") - .HasDatabaseName("IX_Model_ModelSeriesId_ModelType"); - - 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("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ExternalGroupId"); - - 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.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.MediaLifecycleRecord", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") - .WithMany() - .HasForeignKey("VirtualKeyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - 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.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.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.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/20250819013218_SeedModelData.cs b/ConduitLLM.Configuration/Migrations/20250819013218_SeedModelData.cs deleted file mode 100644 index 95f4c9377..000000000 --- a/ConduitLLM.Configuration/Migrations/20250819013218_SeedModelData.cs +++ /dev/null @@ -1,226 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ConduitLLM.Configuration.Migrations -{ - /// - public partial class SeedModelData : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - // Step 1: Insert ModelAuthors - migrationBuilder.InsertData( - table: "ModelAuthors", - columns: new[] { "Id", "Name", "Description", "WebsiteUrl" }, - values: new object[,] - { - { 1, "Meta", "Meta AI (formerly Facebook AI)", "https://ai.meta.com" }, - { 2, "OpenAI", "OpenAI - creators of GPT models", "https://openai.com" }, - { 3, "Groq", "Groq - high-performance inference", "https://groq.com" }, - { 4, "Fireworks", "Fireworks AI - fast inference platform", "https://fireworks.ai" }, - { 5, "Cerebras", "Cerebras - ultra-fast AI compute", "https://cerebras.net" }, - { 6, "DeepInfra", "DeepInfra - serverless AI inference", "https://deepinfra.com" }, - { 7, "ByteDance", "ByteDance - creators of SeeDance and other models", "https://bytedance.com" }, - { 8, "Wan-AI", "Wan AI - video generation models", null }, - { 9, "Moonshot", "Moonshot AI - creators of Kimi models", "https://moonshotai.com" }, - { 10, "ZAI", "ZAI Organization", null }, - { 11, "Zhipu", "Zhipu AI - creators of GLM models", "https://zhipuai.cn" }, - { 12, "Qwen", "Alibaba Qwen Team", "https://qwenlm.github.io" } - }); - - // Step 2: Insert ModelCapabilities (unique combinations) - migrationBuilder.InsertData( - table: "ModelCapabilities", - columns: new[] { "Id", "MaxTokens", "MinTokens", "SupportsVision", "SupportsAudioTranscription", - "SupportsTextToSpeech", "SupportsRealtimeAudio", "SupportsImageGeneration", - "SupportsVideoGeneration", "SupportsEmbeddings", "SupportsChat", - "SupportsFunctionCalling", "SupportsStreaming", "TokenizerType", - "SupportedVoices", "SupportedLanguages", "SupportedFormats" }, - values: new object[,] - { - // LLM Standard (131K context, chat, streaming, function calling) - { 1, 131072, 1, false, false, false, false, false, false, false, true, true, true, 3, null, null, null }, - - // LLM Standard with Vision - { 2, 131072, 1, true, false, false, false, false, false, false, true, true, true, 3, null, null, null }, - - // LLM Extended (262K context) - { 3, 262144, 1, false, false, false, false, false, false, false, true, true, true, 36, null, null, null }, - - // LLM Massive (1M+ context) - { 4, 1048576, 1, true, false, false, false, false, false, false, true, true, true, 3, null, null, null }, - - // Speech-to-Text (Whisper) - { 5, 1024, 1, false, true, false, false, false, false, false, false, false, false, 38, null, "[\"en\",\"es\",\"fr\",\"de\",\"ja\",\"zh\"]", "[\"json\",\"text\",\"srt\",\"vtt\"]" }, - - // Image Generation - { 6, 77, 1, false, false, false, false, true, false, false, false, false, false, 36, null, null, null }, - - // Video Generation - { 7, 77, 1, false, false, false, false, false, true, false, false, false, false, 36, null, null, null }, - - // Content Moderation with Vision - { 8, 1024, 1, true, false, false, false, false, false, false, true, false, true, 21, null, null, null }, - - // LLM Small Context (8K) - { 9, 8192, 1, false, false, false, false, false, false, false, true, true, true, 21, null, null, null }, - - // LLM Medium Context (32K) - { 10, 32768, 1, false, false, false, false, false, false, false, true, true, true, 21, null, null, null }, - - // LLM Large Context (65K) - { 11, 65536, 1, false, false, false, false, false, false, false, true, true, true, 21, null, null, null }, - - // LLM Reasoning Context (64K) - { 12, 65536, 1, false, false, false, false, false, false, false, true, false, true, 36, null, null, null }, - - // Multimodal Vision (65K) - { 13, 65536, 1, true, false, false, false, false, false, false, true, true, true, 36, null, null, null }, - - // LLM Legacy (4K) - { 14, 4096, 1, false, false, false, false, false, false, false, true, false, true, 24, null, null, null } - }); - - // Step 3: Insert ModelSeries - migrationBuilder.InsertData( - table: "ModelSeries", - columns: new[] { "Id", "AuthorId", "Name", "Description", "TokenizerType", "Parameters" }, - values: new object[,] - { - { 1, 1, "LLaMA 3.1", "Meta's LLaMA 3.1 series", 21, "{}" }, - { 2, 1, "LLaMA 3.3", "Meta's LLaMA 3.3 series", 21, "{}" }, - { 3, 1, "LLaMA 4", "Meta's LLaMA 4 series including Scout and Maverick", 21, "{}" }, - { 4, 1, "LLaMA Guard", "Meta's content moderation models", 21, "{}" }, - { 5, 2, "GPT-OSS", "OpenAI's open-source GPT models", 3, "{\"reasoning_effort\":{\"type\":\"select\",\"options\":[{\"value\":\"low\",\"label\":\"Low\"},{\"value\":\"medium\",\"label\":\"Medium\"},{\"value\":\"high\",\"label\":\"High\"}],\"default\":\"medium\",\"label\":\"Reasoning Effort\"}}" }, - { 6, 3, "Whisper", "OpenAI's Whisper speech recognition", 38, "{}" }, - { 7, 4, "Flux", "Fireworks' image generation models", 36, "{\"guidance_scale\":{\"type\":\"slider\",\"min\":1,\"max\":20,\"step\":0.5,\"default\":7.5,\"label\":\"Guidance Scale\"},\"num_inference_steps\":{\"type\":\"slider\",\"min\":20,\"max\":50,\"step\":1,\"default\":30,\"label\":\"Inference Steps\"}}" }, - { 8, 4, "SSD", "Stable Diffusion models", 36, "{\"negative_prompt\":{\"type\":\"text\",\"label\":\"Negative Prompt\"},\"scheduler\":{\"type\":\"select\",\"options\":[{\"value\":\"DDIM\",\"label\":\"DDIM\"},{\"value\":\"DPM\",\"label\":\"DPM\"}],\"default\":\"DDIM\",\"label\":\"Scheduler\"}}" }, - { 9, 12, "Qwen 3", "Alibaba's Qwen 3 series", 36, "{}" }, - { 10, 11, "GLM 4", "Zhipu's GLM 4 series", 35, "{}" }, - { 11, 9, "Kimi", "Moonshot's Kimi series", 36, "{}" }, - { 12, 4, "Chronos", "Fireworks' Chronos series", 24, "{}" }, - { 13, 7, "SeeDance", "ByteDance's video generation", 36, "{}" }, - { 14, 8, "Wan", "Wan AI's video generation", 36, "{\"guidance_scale\":{\"type\":\"slider\",\"min\":1,\"max\":15,\"step\":0.5,\"default\":7,\"label\":\"Guidance Scale\"}}" } - }); - - // Step 4: Insert Models - migrationBuilder.InsertData( - table: "Models", - columns: new[] { "Id", "Name", "Version", "Description", "ModelCardUrl", "ModelType", - "ModelSeriesId", "ModelCapabilitiesId", "IsActive", "CreatedAt", "UpdatedAt" }, - values: new object[,] - { - // Groq Models - { 1, "llama-3.1-8b-instant", "3.1", "Fast 8B parameter LLaMA model", null, 0, 1, 1, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 2, "llama-3.3-70b-versatile", "3.3", "Versatile 70B parameter LLaMA model", null, 0, 2, 1, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 3, "llama-guard-4-12b", "4", "Content moderation model with vision", null, 0, 4, 8, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 4, "whisper-large-v3", "v3", "Large Whisper speech-to-text model", null, 2, 6, 5, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 5, "whisper-large-v3-turbo", "v3-turbo", "Turbo variant of Whisper large", null, 2, 6, 5, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 6, "gpt-oss-120b", "120b", "120B parameter GPT-OSS model", null, 0, 5, 1, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 7, "gpt-oss-20b", "20b", "20B parameter GPT-OSS model", null, 0, 5, 1, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - - // Fireworks Models - { 8, "flux-kontext-pro", "pro", "Professional image-to-image model", null, 1, 7, 6, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 9, "qwen3-coder-480b-a35b-instruct", "480b", "Large coding model with extended context", null, 0, 9, 3, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 10, "glm-4p5", "4.5", "GLM 4.5 model", null, 0, 10, 1, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 11, "kimi-k2-instruct", "k2", "Kimi K2 instruction model", null, 0, 11, 1, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 12, "SSD-1B", "1B", "1B parameter Stable Diffusion", null, 1, 8, 6, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 13, "chronos-hermes-13b-v2", "v2", "13B parameter Chronos model", null, 0, 12, 14, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - - // Cerebras Models - { 14, "llama-4-scout", "4", "Multimodal LLaMA 4 Scout", null, 0, 3, 2, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 15, "llama-3.1-8b", "3.1", "8B parameter LLaMA 3.1", null, 0, 1, 10, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 16, "llama-3.3-70b", "3.3", "70B parameter LLaMA 3.3", null, 0, 2, 11, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 17, "openai-oss", "120b", "OpenAI OSS reasoning model", null, 0, 5, 12, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 18, "qwen-3-32b", "3", "32B Qwen reasoning model", null, 0, 9, 12, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - - // DeepInfra Models - { 19, "Kimi-K2-Instruct", "k2", "Kimi K2 instruction model", null, 0, 11, 1, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 20, "GLM-4.5V", "4.5V", "Multimodal GLM with vision", null, 0, 10, 13, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 21, "Llama-4-Maverick", "4", "LLaMA 4 Maverick with 1M context", null, 0, 3, 4, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 22, "SeeDance-T2V", "1.0", "Text-to-video generation", null, 3, 13, 7, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) }, - { 23, "Wan2.1-T2V-14B", "2.1", "14B text-to-video model", null, 3, 14, 7, true, new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 19, 0, 0, 0, DateTimeKind.Utc) } - }); - - // Step 5: Insert ModelIdentifiers - migrationBuilder.InsertData( - table: "ModelIdentifiers", - columns: new[] { "Id", "ModelId", "Identifier", "Provider", "IsPrimary", "Metadata" }, - values: new object[,] - { - // Primary identifiers from the table - { 1, 1, "llama-3.1-8b-instant", "groq", true, null }, - { 2, 2, "llama-3.3-70b-versatile", "groq", true, null }, - { 3, 3, "meta-llama/llama-guard-4-12b", "groq", true, null }, - { 4, 4, "whisper-large-v3", "groq", true, null }, - { 5, 5, "whisper-large-v3-turbo", "groq", true, null }, - { 6, 6, "openai/gpt-oss-120b", "groq", true, null }, - { 7, 7, "openai/gpt-oss-20b", "groq", true, null }, - - // Fireworks identifiers - { 8, 7, "gpt-oss-20b", "fireworks", true, null }, - { 9, 6, "gpt-oss-120b", "fireworks", true, null }, - { 10, 8, "flux-kontext-pro", "fireworks", true, null }, - { 11, 9, "qwen3-coder-480b-a35b-instruct", "fireworks", true, null }, - { 12, 10, "glm-4p5", "fireworks", true, null }, - { 13, 11, "kimi-k2-instruct", "fireworks", true, null }, - { 14, 12, "SSD-1B", "fireworks", true, null }, - { 15, 13, "chronos-hermes-13b-v2", "fireworks", true, null }, - - // Cerebras identifiers - { 16, 14, "llama-4-scout", "cerebras", true, null }, - { 17, 15, "llama-3.1-8b", "cerebras", true, null }, - { 18, 16, "llama-3.3-70b", "cerebras", true, null }, - { 19, 17, "openai-oss", "cerebras", true, null }, - { 20, 17, "gpt-oss-120b", "cerebras", false, null }, - { 21, 18, "qwen-3-32b", "cerebras", true, null }, - - // DeepInfra identifiers - { 22, 19, "moonshotai/Kimi-K2-Instruct", "deepinfra", true, null }, - { 23, 6, "openai/gpt-oss-120b", "deepinfra", true, null }, - { 24, 20, "zai-org/GLM-4.5V", "deepinfra", true, null }, - { 25, 21, "meta-llama/Llama-4-Maverick", "deepinfra", true, null }, - { 26, 22, "ByteDance/SeeDance-T2V", "deepinfra", true, null }, - { 27, 23, "Wan-AI/Wan2.1-T2V-14B", "deepinfra", true, null } - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - // Remove in reverse order due to foreign key constraints - - // Step 1: Remove ModelIdentifiers - migrationBuilder.DeleteData( - table: "ModelIdentifiers", - keyColumn: "Id", - keyValues: new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27 }); - - // Step 2: Remove Models - migrationBuilder.DeleteData( - table: "Models", - keyColumn: "Id", - keyValues: new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 }); - - // Step 3: Remove ModelSeries - migrationBuilder.DeleteData( - table: "ModelSeries", - keyColumn: "Id", - keyValues: new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 }); - - // Step 4: Remove ModelCapabilities - migrationBuilder.DeleteData( - table: "ModelCapabilities", - keyColumn: "Id", - keyValues: new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 }); - - // Step 5: Remove ModelAuthors - migrationBuilder.DeleteData( - table: "ModelAuthors", - keyColumn: "Id", - keyValues: new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }); - } - } -} diff --git a/ConduitLLM.Configuration/Migrations/20250820004712_RemoveModelTypeAndVersion.Designer.cs b/ConduitLLM.Configuration/Migrations/20250820004712_RemoveModelTypeAndVersion.Designer.cs deleted file mode 100644 index 6ac3d2eb2..000000000 --- a/ConduitLLM.Configuration/Migrations/20250820004712_RemoveModelTypeAndVersion.Designer.cs +++ /dev/null @@ -1,2173 +0,0 @@ -// -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("20250820004712_RemoveModelTypeAndVersion")] - partial class RemoveModelTypeAndVersion - { - /// - 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.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.MediaLifecycleRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileSizeBytes") - .HasColumnType("bigint"); - - b.Property("GeneratedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("GeneratedByModel") - .IsRequired() - .HasColumnType("text"); - - b.Property("GenerationPrompt") - .IsRequired() - .HasColumnType("text"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("MediaType") - .IsRequired() - .HasColumnType("text"); - - b.Property("MediaUrl") - .IsRequired() - .HasColumnType("text"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("StorageKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("VirtualKeyId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("StorageKey") - .IsUnique(); - - b.HasIndex("VirtualKeyId"); - - b.HasIndex("ExpiresAt", "IsDeleted"); - - b.HasIndex("VirtualKeyId", "IsDeleted"); - - b.ToTable("MediaLifecycleRecords"); - }); - - 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.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("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("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ExternalGroupId"); - - 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.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.MediaLifecycleRecord", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") - .WithMany() - .HasForeignKey("VirtualKeyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - 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.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.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.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/20250820004712_RemoveModelTypeAndVersion.cs b/ConduitLLM.Configuration/Migrations/20250820004712_RemoveModelTypeAndVersion.cs deleted file mode 100644 index f19451c2b..000000000 --- a/ConduitLLM.Configuration/Migrations/20250820004712_RemoveModelTypeAndVersion.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ConduitLLM.Configuration.Migrations -{ - /// - public partial class RemoveModelTypeAndVersion : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_Model_ModelSeriesId_ModelType", - table: "Models"); - - migrationBuilder.DropIndex( - name: "IX_Model_ModelType", - table: "Models"); - - migrationBuilder.DropColumn( - name: "ModelType", - table: "Models"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ModelType", - table: "Models", - type: "integer", - nullable: false, - defaultValue: 0); - - migrationBuilder.CreateIndex( - name: "IX_Model_ModelSeriesId_ModelType", - table: "Models", - columns: new[] { "ModelSeriesId", "ModelType" }); - - migrationBuilder.CreateIndex( - name: "IX_Model_ModelType", - table: "Models", - column: "ModelType"); - } - } -} diff --git a/ConduitLLM.Configuration/Migrations/20250821035555_AddApiParametersToModels.Designer.cs b/ConduitLLM.Configuration/Migrations/20250821035555_AddApiParametersToModels.Designer.cs deleted file mode 100644 index 783e853e2..000000000 --- a/ConduitLLM.Configuration/Migrations/20250821035555_AddApiParametersToModels.Designer.cs +++ /dev/null @@ -1,2179 +0,0 @@ -// -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("20250821035555_AddApiParametersToModels")] - partial class AddApiParametersToModels - { - /// - 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.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.MediaLifecycleRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileSizeBytes") - .HasColumnType("bigint"); - - b.Property("GeneratedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("GeneratedByModel") - .IsRequired() - .HasColumnType("text"); - - b.Property("GenerationPrompt") - .IsRequired() - .HasColumnType("text"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("MediaType") - .IsRequired() - .HasColumnType("text"); - - b.Property("MediaUrl") - .IsRequired() - .HasColumnType("text"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("StorageKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("VirtualKeyId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("StorageKey") - .IsUnique(); - - b.HasIndex("VirtualKeyId"); - - b.HasIndex("ExpiresAt", "IsDeleted"); - - b.HasIndex("VirtualKeyId", "IsDeleted"); - - b.ToTable("MediaLifecycleRecords"); - }); - - 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.Model", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ApiParameters") - .HasColumnType("text"); - - 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("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("ApiParameters") - .HasColumnType("text"); - - 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("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ExternalGroupId"); - - 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.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.MediaLifecycleRecord", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") - .WithMany() - .HasForeignKey("VirtualKeyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - 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.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.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.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/20250821035555_AddApiParametersToModels.cs b/ConduitLLM.Configuration/Migrations/20250821035555_AddApiParametersToModels.cs deleted file mode 100644 index 476ab9995..000000000 --- a/ConduitLLM.Configuration/Migrations/20250821035555_AddApiParametersToModels.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ConduitLLM.Configuration.Migrations -{ - /// - public partial class AddApiParametersToModels : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ApiParameters", - table: "ModelSeries", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "ApiParameters", - table: "Models", - type: "text", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ApiParameters", - table: "ModelSeries"); - - migrationBuilder.DropColumn( - name: "ApiParameters", - table: "Models"); - } - } -} diff --git a/ConduitLLM.Configuration/Migrations/20250821035649_SeedApiParametersData.Designer.cs b/ConduitLLM.Configuration/Migrations/20250821035649_SeedApiParametersData.Designer.cs deleted file mode 100644 index a06b10d4e..000000000 --- a/ConduitLLM.Configuration/Migrations/20250821035649_SeedApiParametersData.Designer.cs +++ /dev/null @@ -1,2179 +0,0 @@ -// -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("20250821035649_SeedApiParametersData")] - partial class SeedApiParametersData - { - /// - 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.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.MediaLifecycleRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileSizeBytes") - .HasColumnType("bigint"); - - b.Property("GeneratedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("GeneratedByModel") - .IsRequired() - .HasColumnType("text"); - - b.Property("GenerationPrompt") - .IsRequired() - .HasColumnType("text"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("MediaType") - .IsRequired() - .HasColumnType("text"); - - b.Property("MediaUrl") - .IsRequired() - .HasColumnType("text"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("StorageKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("VirtualKeyId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("StorageKey") - .IsUnique(); - - b.HasIndex("VirtualKeyId"); - - b.HasIndex("ExpiresAt", "IsDeleted"); - - b.HasIndex("VirtualKeyId", "IsDeleted"); - - b.ToTable("MediaLifecycleRecords"); - }); - - 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.Model", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ApiParameters") - .HasColumnType("text"); - - 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("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("ApiParameters") - .HasColumnType("text"); - - 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("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ExternalGroupId"); - - 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.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.MediaLifecycleRecord", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") - .WithMany() - .HasForeignKey("VirtualKeyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - 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.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.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.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/20250821035649_SeedApiParametersData.cs b/ConduitLLM.Configuration/Migrations/20250821035649_SeedApiParametersData.cs deleted file mode 100644 index d374081e2..000000000 --- a/ConduitLLM.Configuration/Migrations/20250821035649_SeedApiParametersData.cs +++ /dev/null @@ -1,200 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ConduitLLM.Configuration.Migrations -{ - /// - public partial class SeedApiParametersData : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - // Update ModelSeries with API parameters - - // Whisper series (ID: 6) - Audio transcription parameters - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 6, - column: "ApiParameters", - value: "[\"language\",\"prompt\",\"response_format\",\"timestamp_granularities\"]" - ); - - // GPT-OSS series (ID: 5) - Reasoning parameters - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 5, - column: "ApiParameters", - value: "[\"reasoning_effort\"]" - ); - - // LLaMA 3.1 series (ID: 1) - Advanced sampling - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 1, - column: "ApiParameters", - value: "[\"min_p\",\"top_k\",\"repetition_penalty\"]" - ); - - // LLaMA 3.3 series (ID: 2) - Advanced sampling - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 2, - column: "ApiParameters", - value: "[\"min_p\",\"top_k\",\"repetition_penalty\"]" - ); - - // LLaMA 4 series (ID: 3) - Advanced sampling with tool support - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 3, - column: "ApiParameters", - value: "[\"min_p\",\"top_k\",\"repetition_penalty\"]" - ); - - // Flux series (ID: 7) - Image generation parameters - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 7, - column: "ApiParameters", - value: "[\"prompt\",\"image_url\",\"guidance_scale\",\"num_inference_steps\"]" - ); - - // SSD series (ID: 8) - Stable Diffusion parameters - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 8, - column: "ApiParameters", - value: "[\"negative_prompt\",\"scheduler\",\"num_inference_steps\",\"guidance_scale\"]" - ); - - // Qwen 3 series (ID: 9) - Advanced models with reasoning - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 9, - column: "ApiParameters", - value: "[\"min_p\",\"top_k\",\"repetition_penalty\"]" - ); - - // GLM 4 series (ID: 10) - Advanced sampling - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 10, - column: "ApiParameters", - value: "[\"min_p\",\"top_k\",\"repetition_penalty\"]" - ); - - // Kimi series (ID: 11) - Advanced sampling - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 11, - column: "ApiParameters", - value: "[\"min_p\",\"top_k\",\"repetition_penalty\"]" - ); - - // SeeDance series (ID: 13) - Video generation - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 13, - column: "ApiParameters", - value: "[\"guidance_scale\",\"seed\",\"negative_prompt\"]" - ); - - // Wan series (ID: 14) - Video generation - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 14, - column: "ApiParameters", - value: "[\"guidance_scale\",\"seed\",\"negative_prompt\"]" - ); - - // Update specific Models with additional parameters - - // Kimi-K2-Instruct on DeepInfra (ID: 19) - Add seed and user - migrationBuilder.UpdateData( - table: "Models", - keyColumn: "Id", - keyValue: 19, - column: "ApiParameters", - value: "[\"seed\",\"user\"]" - ); - - // Llama-4-Maverick on DeepInfra (ID: 21) - Add seed - migrationBuilder.UpdateData( - table: "Models", - keyColumn: "Id", - keyValue: 21, - column: "ApiParameters", - value: "[\"seed\"]" - ); - - // GPT-OSS models on DeepInfra - Add seed and user - migrationBuilder.UpdateData( - table: "Models", - keyColumn: "Id", - keyValue: 6, - column: "ApiParameters", - value: "[\"seed\",\"user\"]" - ); - - migrationBuilder.UpdateData( - table: "Models", - keyColumn: "Id", - keyValue: 7, - column: "ApiParameters", - value: "[\"seed\",\"user\"]" - ); - - // Qwen-3-32b (ID: 18) - Add /no_think support - migrationBuilder.UpdateData( - table: "Models", - keyColumn: "Id", - keyValue: 18, - column: "ApiParameters", - value: "[\"no_think\"]" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - // Revert ModelSeries ApiParameters to null - var modelSeriesIds = new[] { 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 13, 14 }; - foreach (var id in modelSeriesIds) - { - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: id, - column: "ApiParameters", - value: null - ); - } - - // Revert Models ApiParameters to null - var modelIds = new[] { 6, 7, 18, 19, 21 }; - foreach (var id in modelIds) - { - migrationBuilder.UpdateData( - table: "Models", - keyColumn: "Id", - keyValue: id, - column: "ApiParameters", - value: null - ); - } - } - } -} diff --git a/ConduitLLM.Configuration/Migrations/20250821043904_AddApiParametersToModelProviderMapping.Designer.cs b/ConduitLLM.Configuration/Migrations/20250821043904_AddApiParametersToModelProviderMapping.Designer.cs deleted file mode 100644 index c27f91c12..000000000 --- a/ConduitLLM.Configuration/Migrations/20250821043904_AddApiParametersToModelProviderMapping.Designer.cs +++ /dev/null @@ -1,2182 +0,0 @@ -// -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("20250821043904_AddApiParametersToModelProviderMapping")] - partial class AddApiParametersToModelProviderMapping - { - /// - 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.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.MediaLifecycleRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileSizeBytes") - .HasColumnType("bigint"); - - b.Property("GeneratedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("GeneratedByModel") - .IsRequired() - .HasColumnType("text"); - - b.Property("GenerationPrompt") - .IsRequired() - .HasColumnType("text"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("MediaType") - .IsRequired() - .HasColumnType("text"); - - b.Property("MediaUrl") - .IsRequired() - .HasColumnType("text"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("StorageKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("VirtualKeyId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("StorageKey") - .IsUnique(); - - b.HasIndex("VirtualKeyId"); - - b.HasIndex("ExpiresAt", "IsDeleted"); - - b.HasIndex("VirtualKeyId", "IsDeleted"); - - b.ToTable("MediaLifecycleRecords"); - }); - - 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.Model", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ApiParameters") - .HasColumnType("text"); - - 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("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("ApiParameters") - .HasColumnType("text"); - - 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("ApiParameters") - .HasColumnType("text"); - - 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("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ExternalGroupId"); - - 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.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.MediaLifecycleRecord", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") - .WithMany() - .HasForeignKey("VirtualKeyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - 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.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.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.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/20250821043904_AddApiParametersToModelProviderMapping.cs b/ConduitLLM.Configuration/Migrations/20250821043904_AddApiParametersToModelProviderMapping.cs deleted file mode 100644 index fd4d0546c..000000000 --- a/ConduitLLM.Configuration/Migrations/20250821043904_AddApiParametersToModelProviderMapping.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ConduitLLM.Configuration.Migrations -{ - /// - public partial class AddApiParametersToModelProviderMapping : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ApiParameters", - table: "ModelProviderMappings", - type: "text", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ApiParameters", - table: "ModelProviderMappings"); - } - } -} diff --git a/ConduitLLM.Configuration/Migrations/20250821212638_AddReplicateVideoModels.Designer.cs b/ConduitLLM.Configuration/Migrations/20250821212638_AddReplicateVideoModels.Designer.cs deleted file mode 100644 index e25fb2bd6..000000000 --- a/ConduitLLM.Configuration/Migrations/20250821212638_AddReplicateVideoModels.Designer.cs +++ /dev/null @@ -1,2182 +0,0 @@ -// -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("20250821212638_AddReplicateVideoModels")] - partial class AddReplicateVideoModels - { - /// - 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.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.MediaLifecycleRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileSizeBytes") - .HasColumnType("bigint"); - - b.Property("GeneratedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("GeneratedByModel") - .IsRequired() - .HasColumnType("text"); - - b.Property("GenerationPrompt") - .IsRequired() - .HasColumnType("text"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("MediaType") - .IsRequired() - .HasColumnType("text"); - - b.Property("MediaUrl") - .IsRequired() - .HasColumnType("text"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("StorageKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("VirtualKeyId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("StorageKey") - .IsUnique(); - - b.HasIndex("VirtualKeyId"); - - b.HasIndex("ExpiresAt", "IsDeleted"); - - b.HasIndex("VirtualKeyId", "IsDeleted"); - - b.ToTable("MediaLifecycleRecords"); - }); - - 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.Model", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ApiParameters") - .HasColumnType("text"); - - 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("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("ApiParameters") - .HasColumnType("text"); - - 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("ApiParameters") - .HasColumnType("text"); - - 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("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ExternalGroupId"); - - 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.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.MediaLifecycleRecord", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") - .WithMany() - .HasForeignKey("VirtualKeyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - 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.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.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.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/20250821212638_AddReplicateVideoModels.cs b/ConduitLLM.Configuration/Migrations/20250821212638_AddReplicateVideoModels.cs deleted file mode 100644 index a5a1d5e5a..000000000 --- a/ConduitLLM.Configuration/Migrations/20250821212638_AddReplicateVideoModels.cs +++ /dev/null @@ -1,192 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ConduitLLM.Configuration.Migrations -{ - /// - public partial class AddReplicateVideoModels : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - // Step 1: Insert new ModelAuthors for Replicate providers - migrationBuilder.InsertData( - table: "ModelAuthors", - columns: new[] { "Id", "Name", "Description", "WebsiteUrl" }, - values: new object[,] - { - { 15, "Google", "Google AI - Video generation models", "https://ai.google" }, - { 16, "Luma", "Luma AI - Ray video generation series", "https://lumalabs.ai" }, - { 17, "Pixverse", "Pixverse - AI video generation", "https://pixverse.ai" }, - { 18, "KwaiVGI", "Kwai Video Generation Intelligence - Kling series", "https://kling.kuaishou.com" }, - { 19, "Tencent", "Tencent - Hunyuan video models", "https://cloud.tencent.com" }, - { 20, "LeonardoAI", "Leonardo AI - Motion video generation", "https://leonardo.ai" }, - { 21, "Lightricks", "Lightricks - LTX video generation", "https://lightricks.com" }, - { 22, "GenmoAI", "Genmo AI - Mochi video models", "https://genmo.ai" }, - { 23, "Fofr", "Fofr - Video processing models", null }, - { 24, "Replicate Community", "Community contributed models", "https://replicate.com" }, - { 25, "MiniMax", "MiniMax - AI video and model generation", "https://minimax.chat" }, - { 26, "WaveSpeedAI", "WaveSpeed AI - Accelerated AI inference", "https://wavespeed.ai" }, - { 27, "Wan-Video", "Wan Video - AI video generation models", null } - }); - - // Step 2: Insert new ModelSeries for Replicate models (skip existing ones) - migrationBuilder.InsertData( - table: "ModelSeries", - columns: new[] { "Id", "AuthorId", "Name", "Description", "TokenizerType", "Parameters" }, - values: new object[,] - { - { 15, 15, "Veo", "Google's Veo video generation series", 36, @"{""prompt"":{""type"":""text"",""label"":""Prompt"",""required"":true},""resolution"":{""type"":""select"",""options"":[{""value"":""720p"",""label"":""720p""},{""value"":""1080p"",""label"":""1080p""}],""default"":""720p"",""label"":""Resolution""},""seed"":{""type"":""number"",""label"":""Seed"",""min"":0,""max"":999999}}" }, - // SeeDance series already exists as ID 13 with AuthorId 7 (ByteDance) - { 16, 25, "Hailuo", "MiniMax's Hailuo video series", 36, "{}" }, - { 17, 25, "Video-01", "MiniMax's Video-01 series", 36, "{}" }, - { 18, 27, "Wan 2.2", "Wan 2.2 video generation models", 36, "{}" }, - { 19, 26, "Wan 2.1", "WaveSpeed's Wan 2.1 video generation models", 36, "{}" }, - { 20, 16, "Ray", "Luma's Ray video generation series", 36, "{}" }, - { 21, 17, "Pixverse", "Pixverse video generation series", 36, "{}" }, - { 22, 18, "Kling", "Kwai's Kling video generation series", 36, "{}" }, - { 23, 19, "Hunyuan", "Tencent's Hunyuan video models", 36, "{}" }, - { 24, 20, "Motion", "Leonardo's Motion video generation", 36, "{}" }, - { 25, 21, "LTX", "Lightricks' LTX video generation", 36, "{}" }, - { 26, 22, "Mochi", "Genmo's Mochi video models", 36, "{}" } - }); - - // Step 3: Insert new ModelCapabilities for video generation - migrationBuilder.InsertData( - table: "ModelCapabilities", - columns: new[] { "Id", "MaxTokens", "MinTokens", "SupportsVision", "SupportsAudioTranscription", - "SupportsTextToSpeech", "SupportsRealtimeAudio", "SupportsImageGeneration", - "SupportsVideoGeneration", "SupportsEmbeddings", "SupportsChat", - "SupportsFunctionCalling", "SupportsStreaming", "TokenizerType", - "SupportedVoices", "SupportedLanguages", "SupportedFormats" }, - values: new object[,] - { - // Standard Video Generation (Text-to-Video) - { 15, 77, 1, false, false, false, false, false, true, false, false, false, false, 36, null, null, null }, - - // Image-to-Video Generation - { 16, 77, 1, true, false, false, false, false, true, false, false, false, false, 36, null, null, null }, - - // Advanced Video Generation with Audio - { 17, 77, 1, false, false, false, false, false, true, false, false, false, false, 36, null, null, "[\"mp4\",\"webm\"]" } - }); - - var utcNow = new System.DateTime(2025, 8, 21, 0, 0, 0, System.DateTimeKind.Utc); - - // Step 4: Insert Replicate Video Models - migrationBuilder.InsertData( - table: "Models", - columns: new[] { "Id", "Name", "Version", "Description", "ModelCardUrl", - "ModelSeriesId", "ModelCapabilitiesId", "IsActive", "CreatedAt", "UpdatedAt", "ApiParameters" }, - values: new object[,] - { - // Google Veo models - { 24, "veo-3", "3", "Google's flagship Veo 3 text to video model with audio", null, 15, 17, true, utcNow, utcNow, - @"[""prompt"",""image"",""resolution"",""negative_prompt"",""seed""]" }, - - { 25, "veo-3-fast", "3-fast", "Faster and cheaper version of Veo 3 with audio", null, 15, 17, true, utcNow, utcNow, - @"[""prompt"",""image"",""resolution"",""negative_prompt"",""seed""]" }, - - { 26, "veo-2", "2", "State of the art video generation with complex instruction following", null, 15, 15, true, utcNow, utcNow, - @"[""prompt"",""duration"",""aspect_ratio"",""seed"",""image""]" }, - - // ByteDance SeeDance models (using existing SeeDance series ID 13) - { 27, "seedance-1-pro", "1-pro", "Text and image to video, 5s or 10s, up to 1080p", null, 13, 16, true, utcNow, utcNow, - @"[""prompt"",""image"",""duration"",""resolution"",""aspect_ratio"",""camera_fixed"",""seed"",""fps""]" }, - - { 28, "seedance-1-lite", "1-lite", "Video generation with text and image to video support", null, 13, 16, true, utcNow, utcNow, - @"[""prompt"",""image"",""duration"",""resolution"",""aspect_ratio"",""camera_fixed"",""seed"",""fps"",""last_frame_image""]" }, - - // MiniMax models - { 29, "hailuo-02", "02", "Text and image to video, 6s or 10s videos", null, 16, 16, true, utcNow, utcNow, - @"[""prompt"",""duration"",""resolution"",""prompt_optimizer"",""first_frame_image""]" }, - - { 30, "video-01", "01", "Generate 6s videos with prompts or images", null, 17, 16, true, utcNow, utcNow, - @"[""prompt"",""prompt_optimizer"",""first_frame_image"",""subject_reference""]" }, - - // Luma Ray models - { 31, "ray-2-720p", "2-720p", "Generate 5s and 9s 720p videos", null, 20, 15, true, utcNow, utcNow, - @"[""prompt"",""duration"",""aspect_ratio"",""start_image"",""end_image"",""loop"",""concepts""]" }, - - // Pixverse models - { 32, "pixverse-v4.5", "4.5", "Quickly make 5s or 8s videos with enhanced motion", null, 21, 16, true, utcNow, utcNow, - @"[""prompt"",""quality"",""duration"",""aspect_ratio"",""motion_mode"",""style"",""effect"",""seed"",""negative_prompt"",""image"",""last_frame_image"",""sound_effect_switch""]" }, - - // Kling models - { 33, "kling-v2.1", "2.1", "Generate 5s and 10s videos from a starting image", null, 22, 16, true, utcNow, utcNow, - @"[""prompt"",""start_image"",""mode"",""duration"",""negative_prompt""]" }, - - // Tencent Hunyuan - { 34, "hunyuan-video", "1.0", "State-of-the-art text-to-video generation model", null, 23, 15, true, utcNow, utcNow, - @"[""prompt"",""width"",""height"",""video_length"",""infer_steps"",""embedded_guidance_scale"",""fps"",""seed""]" } - }); - - // Step 5: Insert ModelIdentifiers for Replicate - migrationBuilder.InsertData( - table: "ModelIdentifiers", - columns: new[] { "Id", "ModelId", "Identifier", "Provider", "IsPrimary", "Metadata" }, - values: new object[,] - { - // Google models - { 188, 24, "google/veo-3", "replicate", true, null }, - { 189, 25, "google/veo-3-fast", "replicate", true, null }, - { 190, 26, "google/veo-2", "replicate", true, null }, - - // ByteDance models - { 191, 27, "bytedance/seedance-1-pro", "replicate", true, null }, - { 192, 28, "bytedance/seedance-1-lite", "replicate", true, null }, - - // MiniMax models - { 193, 29, "minimax/hailuo-02", "replicate", true, null }, - { 194, 30, "minimax/video-01", "replicate", true, null }, - - // Luma models - { 195, 31, "luma/ray-2-720p", "replicate", true, null }, - - // Pixverse models - { 196, 32, "pixverse/pixverse-v4.5", "replicate", true, null }, - - // Kling models - { 197, 33, "kwaivgi/kling-v2.1", "replicate", true, null }, - - // Tencent models - { 198, 34, "tencent/hunyuan-video", "replicate", true, null } - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - // Remove ModelIdentifiers - migrationBuilder.DeleteData( - table: "ModelIdentifiers", - keyColumn: "Id", - keyValues: new object[] { 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198 }); - - // Remove Models - migrationBuilder.DeleteData( - table: "Models", - keyColumn: "Id", - keyValues: new object[] { 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34 }); - - // Remove ModelCapabilities - migrationBuilder.DeleteData( - table: "ModelCapabilities", - keyColumn: "Id", - keyValues: new object[] { 15, 16, 17 }); - - // Remove ModelSeries (skip ID 13 which already existed) - migrationBuilder.DeleteData( - table: "ModelSeries", - keyColumn: "Id", - keyValues: new object[] { 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 }); - - // Remove ModelAuthors - migrationBuilder.DeleteData( - table: "ModelAuthors", - keyColumn: "Id", - keyValues: new object[] { 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27 }); - } - } -} diff --git a/ConduitLLM.Configuration/Migrations/20250822014413_AddVideoModelParameters.Designer.cs b/ConduitLLM.Configuration/Migrations/20250822014413_AddVideoModelParameters.Designer.cs deleted file mode 100644 index 6a1145ee5..000000000 --- a/ConduitLLM.Configuration/Migrations/20250822014413_AddVideoModelParameters.Designer.cs +++ /dev/null @@ -1,2182 +0,0 @@ -// -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("20250822014413_AddVideoModelParameters")] - partial class AddVideoModelParameters - { - /// - 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.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.MediaLifecycleRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileSizeBytes") - .HasColumnType("bigint"); - - b.Property("GeneratedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("GeneratedByModel") - .IsRequired() - .HasColumnType("text"); - - b.Property("GenerationPrompt") - .IsRequired() - .HasColumnType("text"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("MediaType") - .IsRequired() - .HasColumnType("text"); - - b.Property("MediaUrl") - .IsRequired() - .HasColumnType("text"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("StorageKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("VirtualKeyId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("StorageKey") - .IsUnique(); - - b.HasIndex("VirtualKeyId"); - - b.HasIndex("ExpiresAt", "IsDeleted"); - - b.HasIndex("VirtualKeyId", "IsDeleted"); - - b.ToTable("MediaLifecycleRecords"); - }); - - 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.Model", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ApiParameters") - .HasColumnType("text"); - - 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("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("ApiParameters") - .HasColumnType("text"); - - 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("ApiParameters") - .HasColumnType("text"); - - 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("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ExternalGroupId"); - - 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.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.MediaLifecycleRecord", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") - .WithMany() - .HasForeignKey("VirtualKeyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - 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.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.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.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/20250822014413_AddVideoModelParameters.cs b/ConduitLLM.Configuration/Migrations/20250822014413_AddVideoModelParameters.cs deleted file mode 100644 index ba3998568..000000000 --- a/ConduitLLM.Configuration/Migrations/20250822014413_AddVideoModelParameters.cs +++ /dev/null @@ -1,491 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ConduitLLM.Configuration.Migrations -{ - /// - public partial class AddVideoModelParameters : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - // Update Parameters for Veo series (ID: 15) - Google models - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 15, - column: "Parameters", - value: @"{ - ""duration"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""5"", ""label"": ""5 seconds""}, - {""value"": ""6"", ""label"": ""6 seconds""}, - {""value"": ""7"", ""label"": ""7 seconds""}, - {""value"": ""8"", ""label"": ""8 seconds""} - ], - ""default"": ""5"", - ""label"": ""Duration"" - }, - ""resolution"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""720p"", ""label"": ""720p (1280x720)""} - ], - ""default"": ""720p"", - ""label"": ""Resolution"" - }, - ""aspect_ratio"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""16:9"", ""label"": ""16:9 (Widescreen)""}, - {""value"": ""9:16"", ""label"": ""9:16 (Vertical)""}, - {""value"": ""1:1"", ""label"": ""1:1 (Square)""} - ], - ""default"": ""16:9"", - ""label"": ""Aspect Ratio"" - }, - ""negative_prompt"": { - ""type"": ""textarea"", - ""label"": ""Negative Prompt"", - ""placeholder"": ""Describe what to avoid in the video..."", - ""rows"": 2 - }, - ""seed"": { - ""type"": ""number"", - ""label"": ""Seed"", - ""min"": 0, - ""max"": 999999, - ""placeholder"": ""Random seed for reproducibility"" - }, - ""enhance_prompt"": { - ""type"": ""switch"", - ""label"": ""Enhance Prompt"", - ""default"": true, - ""description"": ""Automatically enhance prompt for better results"" - } - }" - ); - - // Update Parameters for Hailuo series (ID: 16) - MiniMax models - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 16, - column: "Parameters", - value: @"{ - ""duration"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""6"", ""label"": ""6 seconds""}, - {""value"": ""10"", ""label"": ""10 seconds""} - ], - ""default"": ""6"", - ""label"": ""Duration"" - }, - ""resolution"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""512p"", ""label"": ""512p""}, - {""value"": ""768p"", ""label"": ""768p""}, - {""value"": ""1080p"", ""label"": ""1080p (Full HD)""} - ], - ""default"": ""768p"", - ""label"": ""Resolution"" - }, - ""prompt_optimizer"": { - ""type"": ""switch"", - ""label"": ""Prompt Optimizer"", - ""default"": true, - ""description"": ""Use AI to optimize your prompt"" - } - }" - ); - - // Update Parameters for Video-01 series (ID: 17) - MiniMax models - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 17, - column: "Parameters", - value: @"{ - ""prompt_optimizer"": { - ""type"": ""switch"", - ""label"": ""Prompt Optimizer"", - ""default"": true, - ""description"": ""Use AI to optimize your prompt"" - } - }" - ); - - // Update Parameters for SeeDance series (ID: 13) - ByteDance models - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 13, - column: "Parameters", - value: @"{ - ""duration"": { - ""type"": ""select"", - ""options"": [ - {""value"": 5, ""label"": ""5 seconds""}, - {""value"": 10, ""label"": ""10 seconds""} - ], - ""default"": 5, - ""label"": ""Duration"" - }, - ""resolution"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""480p"", ""label"": ""480p (SD)""}, - {""value"": ""720p"", ""label"": ""720p (HD)""}, - {""value"": ""1080p"", ""label"": ""1080p (Full HD)""} - ], - ""default"": ""720p"", - ""label"": ""Resolution"" - }, - ""aspect_ratio"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""16:9"", ""label"": ""16:9 (Widescreen)""}, - {""value"": ""9:16"", ""label"": ""9:16 (Vertical)""}, - {""value"": ""4:3"", ""label"": ""4:3 (Standard)""}, - {""value"": ""1:1"", ""label"": ""1:1 (Square)""}, - {""value"": ""3:4"", ""label"": ""3:4 (Portrait)""}, - {""value"": ""21:9"", ""label"": ""21:9 (Ultrawide)""}, - {""value"": ""9:21"", ""label"": ""9:21 (Ultra Tall)""} - ], - ""default"": ""16:9"", - ""label"": ""Aspect Ratio"" - }, - ""fps"": { - ""type"": ""select"", - ""options"": [ - {""value"": 24, ""label"": ""24 FPS""} - ], - ""default"": 24, - ""label"": ""Frame Rate"" - }, - ""camera_fixed"": { - ""type"": ""switch"", - ""label"": ""Fixed Camera"", - ""default"": false, - ""description"": ""Keep camera position fixed"" - }, - ""seed"": { - ""type"": ""number"", - ""label"": ""Seed"", - ""min"": 0, - ""max"": 999999, - ""placeholder"": ""Random seed for reproducibility"" - } - }" - ); - - // Update Parameters for Ray series (ID: 20) - Luma models - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 20, - column: "Parameters", - value: @"{ - ""duration"": { - ""type"": ""select"", - ""options"": [ - {""value"": 5, ""label"": ""5 seconds""}, - {""value"": 9, ""label"": ""9 seconds""} - ], - ""default"": 5, - ""label"": ""Duration"" - }, - ""aspect_ratio"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""1:1"", ""label"": ""1:1 (Square)""}, - {""value"": ""3:4"", ""label"": ""3:4 (Portrait)""}, - {""value"": ""4:3"", ""label"": ""4:3 (Standard)""}, - {""value"": ""9:16"", ""label"": ""9:16 (Vertical)""}, - {""value"": ""16:9"", ""label"": ""16:9 (Widescreen)""}, - {""value"": ""9:21"", ""label"": ""9:21 (Ultra Tall)""}, - {""value"": ""21:9"", ""label"": ""21:9 (Ultrawide)""} - ], - ""default"": ""16:9"", - ""label"": ""Aspect Ratio"" - }, - ""loop"": { - ""type"": ""switch"", - ""label"": ""Loop Video"", - ""default"": false, - ""description"": ""Create a seamless loop"" - } - }" - ); - - // Update Parameters for Pixverse series (ID: 21) - Pixverse models - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 21, - column: "Parameters", - value: @"{ - ""duration"": { - ""type"": ""select"", - ""options"": [ - {""value"": 5, ""label"": ""5 seconds""}, - {""value"": 8, ""label"": ""8 seconds""} - ], - ""default"": 5, - ""label"": ""Duration"" - }, - ""quality"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""360p"", ""label"": ""360p (Low)""}, - {""value"": ""540p"", ""label"": ""540p (Medium)""}, - {""value"": ""720p"", ""label"": ""720p (HD)""}, - {""value"": ""1080p"", ""label"": ""1080p (Full HD)""} - ], - ""default"": ""540p"", - ""label"": ""Quality"" - }, - ""aspect_ratio"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""16:9"", ""label"": ""16:9 (Widescreen)""}, - {""value"": ""9:16"", ""label"": ""9:16 (Vertical)""}, - {""value"": ""1:1"", ""label"": ""1:1 (Square)""} - ], - ""default"": ""16:9"", - ""label"": ""Aspect Ratio"" - }, - ""motion_mode"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""normal"", ""label"": ""Normal""}, - {""value"": ""smooth"", ""label"": ""Smooth""} - ], - ""default"": ""normal"", - ""label"": ""Motion Mode"" - }, - ""style"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""None"", ""label"": ""None""}, - {""value"": ""anime"", ""label"": ""Anime""}, - {""value"": ""3d_animation"", ""label"": ""3D Animation""}, - {""value"": ""clay"", ""label"": ""Clay""}, - {""value"": ""cyberpunk"", ""label"": ""Cyberpunk""}, - {""value"": ""comic"", ""label"": ""Comic""} - ], - ""default"": ""None"", - ""label"": ""Style"" - }, - ""negative_prompt"": { - ""type"": ""textarea"", - ""label"": ""Negative Prompt"", - ""placeholder"": ""Elements to avoid..."", - ""rows"": 2 - }, - ""seed"": { - ""type"": ""number"", - ""label"": ""Seed"", - ""min"": 0, - ""max"": 999999, - ""placeholder"": ""Random seed"" - } - }" - ); - - // Update Parameters for Kling series (ID: 22) - KwaiVGI models - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 22, - column: "Parameters", - value: @"{ - ""duration"": { - ""type"": ""select"", - ""options"": [ - {""value"": 5, ""label"": ""5 seconds""}, - {""value"": 10, ""label"": ""10 seconds""} - ], - ""default"": 5, - ""label"": ""Duration"" - }, - ""aspect_ratio"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""16:9"", ""label"": ""16:9 (Widescreen)""}, - {""value"": ""9:16"", ""label"": ""9:16 (Vertical)""}, - {""value"": ""1:1"", ""label"": ""1:1 (Square)""} - ], - ""default"": ""16:9"", - ""label"": ""Aspect Ratio"" - }, - ""negative_prompt"": { - ""type"": ""textarea"", - ""label"": ""Negative Prompt"", - ""placeholder"": ""What to avoid..."", - ""rows"": 2 - }, - ""cfg_scale"": { - ""type"": ""slider"", - ""min"": 0, - ""max"": 1, - ""step"": 0.1, - ""default"": 0.5, - ""label"": ""CFG Scale"", - ""description"": ""Classifier-free guidance scale"" - } - }" - ); - - // Update Parameters for Hunyuan series (ID: 23) - Tencent models - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 23, - column: "Parameters", - value: @"{ - ""width"": { - ""type"": ""number"", - ""min"": 16, - ""max"": 1280, - ""default"": 864, - ""label"": ""Width"", - ""description"": ""Video width in pixels"" - }, - ""height"": { - ""type"": ""number"", - ""min"": 16, - ""max"": 1280, - ""default"": 480, - ""label"": ""Height"", - ""description"": ""Video height in pixels"" - }, - ""video_length"": { - ""type"": ""slider"", - ""min"": 1, - ""max"": 200, - ""default"": 129, - ""label"": ""Video Length"", - ""description"": ""Number of frames to generate"" - }, - ""fps"": { - ""type"": ""number"", - ""min"": 1, - ""max"": 60, - ""default"": 24, - ""label"": ""Frame Rate"" - }, - ""infer_steps"": { - ""type"": ""number"", - ""min"": 1, - ""max"": 100, - ""default"": 50, - ""label"": ""Inference Steps"", - ""description"": ""Number of denoising steps"" - }, - ""embedded_guidance_scale"": { - ""type"": ""slider"", - ""min"": 1, - ""max"": 10, - ""step"": 0.5, - ""default"": 6, - ""label"": ""Guidance Scale"" - }, - ""negative_prompt"": { - ""type"": ""textarea"", - ""label"": ""Negative Prompt"", - ""placeholder"": ""Elements to avoid..."", - ""rows"": 2 - }, - ""seed"": { - ""type"": ""number"", - ""label"": ""Seed"", - ""min"": 0, - ""max"": 999999, - ""placeholder"": ""Random seed"" - } - }" - ); - - // Add new ModelSeries for models that don't have series yet - // Note: Wan series (ID: 14) already has parameters, keeping them as is - - // Add new series for models that need them (if any) - // Based on the migration data, we already have the necessary series - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - // Revert Veo series parameters - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 15, - column: "Parameters", - value: @"{""prompt"":{""type"":""text"",""label"":""Prompt"",""required"":true},""resolution"":{""type"":""select"",""options"":[{""value"":""720p"",""label"":""720p""},{""value"":""1080p"",""label"":""1080p""}],""default"":""720p"",""label"":""Resolution""},""seed"":{""type"":""number"",""label"":""Seed"",""min"":0,""max"":999999}}" - ); - - // Revert other series to empty parameters - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 16, - column: "Parameters", - value: "{}" - ); - - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 17, - column: "Parameters", - value: "{}" - ); - - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 13, - column: "Parameters", - value: "{}" - ); - - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 20, - column: "Parameters", - value: "{}" - ); - - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 21, - column: "Parameters", - value: "{}" - ); - - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 22, - column: "Parameters", - value: "{}" - ); - - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 23, - column: "Parameters", - value: "{}" - ); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Configuration/Migrations/20250822023805_FixVideoParameterNumericValues.Designer.cs b/ConduitLLM.Configuration/Migrations/20250822023805_FixVideoParameterNumericValues.Designer.cs deleted file mode 100644 index 4f844be5b..000000000 --- a/ConduitLLM.Configuration/Migrations/20250822023805_FixVideoParameterNumericValues.Designer.cs +++ /dev/null @@ -1,2182 +0,0 @@ -// -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("20250822023805_FixVideoParameterNumericValues")] - partial class FixVideoParameterNumericValues - { - /// - 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.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.MediaLifecycleRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileSizeBytes") - .HasColumnType("bigint"); - - b.Property("GeneratedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("GeneratedByModel") - .IsRequired() - .HasColumnType("text"); - - b.Property("GenerationPrompt") - .IsRequired() - .HasColumnType("text"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("MediaType") - .IsRequired() - .HasColumnType("text"); - - b.Property("MediaUrl") - .IsRequired() - .HasColumnType("text"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("StorageKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("VirtualKeyId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("StorageKey") - .IsUnique(); - - b.HasIndex("VirtualKeyId"); - - b.HasIndex("ExpiresAt", "IsDeleted"); - - b.HasIndex("VirtualKeyId", "IsDeleted"); - - b.ToTable("MediaLifecycleRecords"); - }); - - 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.Model", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ApiParameters") - .HasColumnType("text"); - - 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("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("ApiParameters") - .HasColumnType("text"); - - 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("ApiParameters") - .HasColumnType("text"); - - 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("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ExternalGroupId"); - - 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.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.MediaLifecycleRecord", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") - .WithMany() - .HasForeignKey("VirtualKeyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - 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.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.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.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/20250822023805_FixVideoParameterNumericValues.cs b/ConduitLLM.Configuration/Migrations/20250822023805_FixVideoParameterNumericValues.cs deleted file mode 100644 index 5d44224b4..000000000 --- a/ConduitLLM.Configuration/Migrations/20250822023805_FixVideoParameterNumericValues.cs +++ /dev/null @@ -1,223 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ConduitLLM.Configuration.Migrations -{ - /// - public partial class FixVideoParameterNumericValues : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - // Fix SeeDance series (ID: 19) - fps numeric value - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 19, - column: "Parameters", - value: @"{ - ""aspect_ratio"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""16:9"", ""label"": ""16:9 (Widescreen)""}, - {""value"": ""9:16"", ""label"": ""9:16 (Vertical)""}, - {""value"": ""4:3"", ""label"": ""4:3 (Standard)""}, - {""value"": ""1:1"", ""label"": ""1:1 (Square)""}, - {""value"": ""3:4"", ""label"": ""3:4 (Portrait)""}, - {""value"": ""21:9"", ""label"": ""21:9 (Ultrawide)""}, - {""value"": ""9:21"", ""label"": ""9:21 (Ultra Tall)""} - ], - ""default"": ""16:9"", - ""label"": ""Aspect Ratio"" - }, - ""fps"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""24"", ""label"": ""24 FPS""} - ], - ""default"": ""24"", - ""label"": ""Frame Rate"" - }, - ""camera_fixed"": { - ""type"": ""switch"", - ""label"": ""Fixed Camera"", - ""default"": false, - ""description"": ""Keep camera position fixed"" - }, - ""seed"": { - ""type"": ""number"", - ""label"": ""Seed"", - ""min"": 0, - ""max"": 999999, - ""placeholder"": ""Random seed for reproducibility"" - } - }" - ); - - // Fix Ray series (ID: 20) - duration numeric values - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 20, - column: "Parameters", - value: @"{ - ""duration"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""5"", ""label"": ""5 seconds""}, - {""value"": ""9"", ""label"": ""9 seconds""} - ], - ""default"": ""5"", - ""label"": ""Duration"" - }, - ""aspect_ratio"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""1:1"", ""label"": ""1:1 (Square)""}, - {""value"": ""3:4"", ""label"": ""3:4 (Portrait)""}, - {""value"": ""4:3"", ""label"": ""4:3 (Standard)""}, - {""value"": ""9:16"", ""label"": ""9:16 (Vertical)""}, - {""value"": ""16:9"", ""label"": ""16:9 (Widescreen)""}, - {""value"": ""9:21"", ""label"": ""9:21 (Ultra Tall)""}, - {""value"": ""21:9"", ""label"": ""21:9 (Ultrawide)""} - ], - ""default"": ""16:9"", - ""label"": ""Aspect Ratio"" - }, - ""loop"": { - ""type"": ""switch"", - ""label"": ""Loop Video"", - ""default"": false, - ""description"": ""Create a seamless loop"" - } - }" - ); - - // Fix Pixverse series (ID: 21) - duration numeric values - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 21, - column: "Parameters", - value: @"{ - ""duration"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""5"", ""label"": ""5 seconds""}, - {""value"": ""8"", ""label"": ""8 seconds""} - ], - ""default"": ""5"", - ""label"": ""Duration"" - }, - ""quality"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""360p"", ""label"": ""360p (Low)""}, - {""value"": ""540p"", ""label"": ""540p (Medium)""}, - {""value"": ""720p"", ""label"": ""720p (HD)""}, - {""value"": ""1080p"", ""label"": ""1080p (Full HD)""} - ], - ""default"": ""540p"", - ""label"": ""Quality"" - }, - ""aspect_ratio"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""16:9"", ""label"": ""16:9 (Widescreen)""}, - {""value"": ""9:16"", ""label"": ""9:16 (Vertical)""}, - {""value"": ""1:1"", ""label"": ""1:1 (Square)""} - ], - ""default"": ""16:9"", - ""label"": ""Aspect Ratio"" - }, - ""motion_mode"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""normal"", ""label"": ""Normal""}, - {""value"": ""smooth"", ""label"": ""Smooth""} - ], - ""default"": ""normal"", - ""label"": ""Motion Mode"" - }, - ""style"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""None"", ""label"": ""None""}, - {""value"": ""anime"", ""label"": ""Anime""}, - {""value"": ""3d_animation"", ""label"": ""3D Animation""}, - {""value"": ""clay"", ""label"": ""Clay""}, - {""value"": ""cyberpunk"", ""label"": ""Cyberpunk""}, - {""value"": ""comic"", ""label"": ""Comic""} - ], - ""default"": ""None"", - ""label"": ""Style"" - }, - ""negative_prompt"": { - ""type"": ""textarea"", - ""label"": ""Negative Prompt"", - ""placeholder"": ""Elements to avoid..."", - ""rows"": 2 - }, - ""seed"": { - ""type"": ""number"", - ""label"": ""Seed"", - ""min"": 0, - ""max"": 999999, - ""placeholder"": ""Random seed"" - } - }" - ); - - // Fix Kling series (ID: 22) - duration numeric values - migrationBuilder.UpdateData( - table: "ModelSeries", - keyColumn: "Id", - keyValue: 22, - column: "Parameters", - value: @"{ - ""duration"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""5"", ""label"": ""5 seconds""}, - {""value"": ""10"", ""label"": ""10 seconds""} - ], - ""default"": ""5"", - ""label"": ""Duration"" - }, - ""aspect_ratio"": { - ""type"": ""select"", - ""options"": [ - {""value"": ""16:9"", ""label"": ""16:9 (Widescreen)""}, - {""value"": ""9:16"", ""label"": ""9:16 (Vertical)""}, - {""value"": ""1:1"", ""label"": ""1:1 (Square)""} - ], - ""default"": ""16:9"", - ""label"": ""Aspect Ratio"" - }, - ""negative_prompt"": { - ""type"": ""textarea"", - ""label"": ""Negative Prompt"", - ""placeholder"": ""What to avoid..."", - ""rows"": 2 - }, - ""cfg_scale"": { - ""type"": ""slider"", - ""min"": 0, - ""max"": 1, - ""step"": 0.1, - ""default"": 0.5, - ""label"": ""CFG Scale"", - ""description"": ""Classifier-free guidance scale"" - } - }" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/ConduitLLM.Configuration/Migrations/20250826181103_AddBillingAuditEvents.Designer.cs b/ConduitLLM.Configuration/Migrations/20250826181103_AddBillingAuditEvents.Designer.cs deleted file mode 100644 index 44fbba3b2..000000000 --- a/ConduitLLM.Configuration/Migrations/20250826181103_AddBillingAuditEvents.Designer.cs +++ /dev/null @@ -1,2243 +0,0 @@ -// -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("20250826181103_AddBillingAuditEvents")] - partial class AddBillingAuditEvents - { - /// - 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"); - - 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") - .HasColumnType("timestamp with time zone"); - - b.Property("UsageJson") - .HasColumnType("jsonb"); - - b.Property("VirtualKeyId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("VirtualKeyId"); - - b.ToTable("BillingAuditEvents"); - }); - - 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.MediaLifecycleRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileSizeBytes") - .HasColumnType("bigint"); - - b.Property("GeneratedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("GeneratedByModel") - .IsRequired() - .HasColumnType("text"); - - b.Property("GenerationPrompt") - .IsRequired() - .HasColumnType("text"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("MediaType") - .IsRequired() - .HasColumnType("text"); - - b.Property("MediaUrl") - .IsRequired() - .HasColumnType("text"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("StorageKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("VirtualKeyId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("StorageKey") - .IsUnique(); - - b.HasIndex("VirtualKeyId"); - - b.HasIndex("ExpiresAt", "IsDeleted"); - - b.HasIndex("VirtualKeyId", "IsDeleted"); - - b.ToTable("MediaLifecycleRecords"); - }); - - 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.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("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ExternalGroupId"); - - 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"); - - 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.MediaLifecycleRecord", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") - .WithMany() - .HasForeignKey("VirtualKeyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - 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.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.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.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/20250826181103_AddBillingAuditEvents.cs b/ConduitLLM.Configuration/Migrations/20250826181103_AddBillingAuditEvents.cs deleted file mode 100644 index 0428b44f7..000000000 --- a/ConduitLLM.Configuration/Migrations/20250826181103_AddBillingAuditEvents.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ConduitLLM.Configuration.Migrations -{ - /// - public partial class AddBillingAuditEvents : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "BillingAuditEvents", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Timestamp = table.Column(type: "timestamp with time zone", nullable: false), - EventType = table.Column(type: "integer", nullable: false), - VirtualKeyId = table.Column(type: "integer", nullable: true), - Model = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - RequestId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - UsageJson = table.Column(type: "jsonb", nullable: true), - CalculatedCost = table.Column(type: "numeric(10,6)", nullable: true), - FailureReason = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - ProviderType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), - HttpStatusCode = table.Column(type: "integer", nullable: true), - MetadataJson = table.Column(type: "jsonb", nullable: true), - RequestPath = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - IsEstimated = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_BillingAuditEvents", x => x.Id); - table.ForeignKey( - name: "FK_BillingAuditEvents_VirtualKeys_VirtualKeyId", - column: x => x.VirtualKeyId, - principalTable: "VirtualKeys", - principalColumn: "Id"); - }); - - migrationBuilder.CreateIndex( - name: "IX_BillingAuditEvents_VirtualKeyId", - table: "BillingAuditEvents", - column: "VirtualKeyId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "BillingAuditEvents"); - } - } -} diff --git a/ConduitLLM.Configuration/Migrations/20250827202307_AddMediaRetentionPolicies.Designer.cs b/ConduitLLM.Configuration/Migrations/20250827202307_AddMediaRetentionPolicies.Designer.cs deleted file mode 100644 index e05aa8b34..000000000 --- a/ConduitLLM.Configuration/Migrations/20250827202307_AddMediaRetentionPolicies.Designer.cs +++ /dev/null @@ -1,2265 +0,0 @@ -// -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("20250827202307_AddMediaRetentionPolicies")] - partial class AddMediaRetentionPolicies - { - /// - 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/20250827202307_AddMediaRetentionPolicies.cs b/ConduitLLM.Configuration/Migrations/20250827202307_AddMediaRetentionPolicies.cs deleted file mode 100644 index f8f39b2e3..000000000 --- a/ConduitLLM.Configuration/Migrations/20250827202307_AddMediaRetentionPolicies.cs +++ /dev/null @@ -1,341 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace ConduitLLM.Configuration.Migrations -{ - /// - public partial class AddMediaRetentionPolicies : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - // Only drop if the table exists (for fresh installs it won't) - migrationBuilder.Sql(@"DROP TABLE IF EXISTS ""MediaLifecycleRecords"" CASCADE;"); - - migrationBuilder.AddColumn( - name: "MediaRetentionPolicyId", - table: "VirtualKeyGroups", - type: "integer", - nullable: true); - - // Only modify columns if the table exists - migrationBuilder.Sql(@" - DO $$ - BEGIN - IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'BillingAuditEvents') THEN - ALTER TABLE ""BillingAuditEvents"" - ALTER COLUMN ""Timestamp"" SET DEFAULT CURRENT_TIMESTAMP; - - ALTER TABLE ""BillingAuditEvents"" - ALTER COLUMN ""Id"" SET DEFAULT gen_random_uuid(); - END IF; - END $$; - "); - - migrationBuilder.CreateTable( - name: "MediaRetentionPolicies", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - PositiveBalanceRetentionDays = table.Column(type: "integer", nullable: false), - ZeroBalanceRetentionDays = table.Column(type: "integer", nullable: false), - NegativeBalanceRetentionDays = table.Column(type: "integer", nullable: false), - SoftDeleteGracePeriodDays = table.Column(type: "integer", nullable: false), - RespectRecentAccess = table.Column(type: "boolean", nullable: false), - RecentAccessWindowDays = table.Column(type: "integer", nullable: false), - IsProTier = table.Column(type: "boolean", nullable: false), - IsDefault = table.Column(type: "boolean", nullable: false), - MaxStorageSizeBytes = table.Column(type: "bigint", nullable: true), - MaxFileCount = table.Column(type: "integer", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - IsActive = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_MediaRetentionPolicies", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_VirtualKeyGroups_MediaRetentionPolicyId", - table: "VirtualKeyGroups", - column: "MediaRetentionPolicyId"); - - // Only create indexes if the table exists - migrationBuilder.Sql(@" - DO $$ - BEGIN - IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'BillingAuditEvents') THEN - CREATE INDEX IF NOT EXISTS ""IX_BillingAuditEvents_EventType"" ON ""BillingAuditEvents"" (""EventType""); - CREATE INDEX IF NOT EXISTS ""IX_BillingAuditEvents_EventType_Timestamp"" ON ""BillingAuditEvents"" (""EventType"", ""Timestamp""); - CREATE INDEX IF NOT EXISTS ""IX_BillingAuditEvents_RequestId"" ON ""BillingAuditEvents"" (""RequestId""); - CREATE INDEX IF NOT EXISTS ""IX_BillingAuditEvents_Timestamp"" ON ""BillingAuditEvents"" (""Timestamp""); - CREATE INDEX IF NOT EXISTS ""IX_BillingAuditEvents_VirtualKeyId_Timestamp"" ON ""BillingAuditEvents"" (""VirtualKeyId"", ""Timestamp""); - END IF; - END $$; - "); - - migrationBuilder.CreateIndex( - name: "IX_MediaRetentionPolicies_IsActive", - table: "MediaRetentionPolicies", - column: "IsActive"); - - migrationBuilder.CreateIndex( - name: "IX_MediaRetentionPolicies_IsDefault", - table: "MediaRetentionPolicies", - column: "IsDefault", - unique: true, - filter: "\"IsDefault\" = true"); - - migrationBuilder.CreateIndex( - name: "IX_MediaRetentionPolicies_Name", - table: "MediaRetentionPolicies", - column: "Name", - unique: true); - - // Only add foreign key if the table exists - migrationBuilder.Sql(@" - DO $$ - BEGIN - IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'BillingAuditEvents') THEN - ALTER TABLE ""BillingAuditEvents"" - ADD CONSTRAINT ""FK_BillingAuditEvents_VirtualKeys_VirtualKeyId"" - FOREIGN KEY (""VirtualKeyId"") REFERENCES ""VirtualKeys"" (""Id"") ON DELETE SET NULL; - END IF; - EXCEPTION WHEN duplicate_object THEN - NULL; -- Ignore if constraint already exists - END $$; - "); - - migrationBuilder.AddForeignKey( - name: "FK_VirtualKeyGroups_MediaRetentionPolicies_MediaRetentionPolic~", - table: "VirtualKeyGroups", - column: "MediaRetentionPolicyId", - principalTable: "MediaRetentionPolicies", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - - // Insert default retention policy - migrationBuilder.Sql(@" - INSERT INTO ""MediaRetentionPolicies"" ( - ""Id"", - ""Name"", - ""Description"", - ""PositiveBalanceRetentionDays"", - ""ZeroBalanceRetentionDays"", - ""NegativeBalanceRetentionDays"", - ""SoftDeleteGracePeriodDays"", - ""RespectRecentAccess"", - ""RecentAccessWindowDays"", - ""IsProTier"", - ""IsDefault"", - ""MaxStorageSizeBytes"", - ""MaxFileCount"", - ""CreatedAt"", - ""UpdatedAt"", - ""IsActive"" - ) - SELECT - 1, - 'Default', - 'Standard retention policy for all virtual key groups', - 60, -- 60 days for positive balance - 14, -- 14 days for zero balance - 3, -- 3 days for negative balance - 7, -- 7 days soft delete grace period - true, -- Respect recent access - 7, -- 7 days recent access window - false, -- Not pro tier - true, -- Is default policy - NULL, -- No storage limit - NULL, -- No file count limit - NOW(), - NOW(), - true - WHERE NOT EXISTS ( - SELECT 1 FROM ""MediaRetentionPolicies"" WHERE ""IsDefault"" = true - ) - "); - - // Insert pro tier retention policy - migrationBuilder.Sql(@" - INSERT INTO ""MediaRetentionPolicies"" ( - ""Id"", - ""Name"", - ""Description"", - ""PositiveBalanceRetentionDays"", - ""ZeroBalanceRetentionDays"", - ""NegativeBalanceRetentionDays"", - ""SoftDeleteGracePeriodDays"", - ""RespectRecentAccess"", - ""RecentAccessWindowDays"", - ""IsProTier"", - ""IsDefault"", - ""MaxStorageSizeBytes"", - ""MaxFileCount"", - ""CreatedAt"", - ""UpdatedAt"", - ""IsActive"" - ) - SELECT - 2, - 'Pro Tier', - 'Extended retention policy for pro tier customers', - 180, -- 180 days for positive balance - 30, -- 30 days for zero balance - 7, -- 7 days for negative balance - 14, -- 14 days soft delete grace period - true, -- Respect recent access - 14, -- 14 days recent access window - true, -- Is pro tier - false, -- Not default - 10737418240, -- 10GB storage limit - 10000, -- 10,000 file limit - NOW(), - NOW(), - true - WHERE NOT EXISTS ( - SELECT 1 FROM ""MediaRetentionPolicies"" WHERE ""Name"" = 'Pro Tier' - ) - "); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_BillingAuditEvents_VirtualKeys_VirtualKeyId", - table: "BillingAuditEvents"); - - migrationBuilder.DropForeignKey( - name: "FK_VirtualKeyGroups_MediaRetentionPolicies_MediaRetentionPolic~", - table: "VirtualKeyGroups"); - - migrationBuilder.DropTable( - name: "MediaRetentionPolicies"); - - migrationBuilder.DropIndex( - name: "IX_VirtualKeyGroups_MediaRetentionPolicyId", - table: "VirtualKeyGroups"); - - migrationBuilder.DropIndex( - name: "IX_BillingAuditEvents_EventType", - table: "BillingAuditEvents"); - - migrationBuilder.DropIndex( - name: "IX_BillingAuditEvents_EventType_Timestamp", - table: "BillingAuditEvents"); - - migrationBuilder.DropIndex( - name: "IX_BillingAuditEvents_RequestId", - table: "BillingAuditEvents"); - - migrationBuilder.DropIndex( - name: "IX_BillingAuditEvents_Timestamp", - table: "BillingAuditEvents"); - - migrationBuilder.DropIndex( - name: "IX_BillingAuditEvents_VirtualKeyId_Timestamp", - table: "BillingAuditEvents"); - - migrationBuilder.DropColumn( - name: "MediaRetentionPolicyId", - table: "VirtualKeyGroups"); - - migrationBuilder.AlterColumn( - name: "Timestamp", - table: "BillingAuditEvents", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldDefaultValueSql: "CURRENT_TIMESTAMP"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "BillingAuditEvents", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldDefaultValueSql: "gen_random_uuid()"); - - migrationBuilder.CreateTable( - name: "MediaLifecycleRecords", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - VirtualKeyId = table.Column(type: "integer", nullable: false), - ContentType = table.Column(type: "text", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), - FileSizeBytes = table.Column(type: "bigint", nullable: false), - GeneratedAt = table.Column(type: "timestamp with time zone", nullable: false), - GeneratedByModel = table.Column(type: "text", nullable: false), - GenerationPrompt = table.Column(type: "text", nullable: false), - IsDeleted = table.Column(type: "boolean", nullable: false), - MediaType = table.Column(type: "text", nullable: false), - MediaUrl = table.Column(type: "text", nullable: false), - Metadata = table.Column(type: "text", nullable: true), - StorageKey = table.Column(type: "text", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_MediaLifecycleRecords", x => x.Id); - table.ForeignKey( - name: "FK_MediaLifecycleRecords_VirtualKeys_VirtualKeyId", - column: x => x.VirtualKeyId, - principalTable: "VirtualKeys", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_MediaLifecycleRecords_CreatedAt", - table: "MediaLifecycleRecords", - column: "CreatedAt"); - - migrationBuilder.CreateIndex( - name: "IX_MediaLifecycleRecords_ExpiresAt", - table: "MediaLifecycleRecords", - column: "ExpiresAt"); - - migrationBuilder.CreateIndex( - name: "IX_MediaLifecycleRecords_ExpiresAt_IsDeleted", - table: "MediaLifecycleRecords", - columns: new[] { "ExpiresAt", "IsDeleted" }); - - migrationBuilder.CreateIndex( - name: "IX_MediaLifecycleRecords_StorageKey", - table: "MediaLifecycleRecords", - column: "StorageKey", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_MediaLifecycleRecords_VirtualKeyId", - table: "MediaLifecycleRecords", - column: "VirtualKeyId"); - - migrationBuilder.CreateIndex( - name: "IX_MediaLifecycleRecords_VirtualKeyId_IsDeleted", - table: "MediaLifecycleRecords", - columns: new[] { "VirtualKeyId", "IsDeleted" }); - - migrationBuilder.AddForeignKey( - name: "FK_BillingAuditEvents_VirtualKeys_VirtualKeyId", - table: "BillingAuditEvents", - column: "VirtualKeyId", - principalTable: "VirtualKeys", - principalColumn: "Id"); - } - } -} diff --git a/ConduitLLM.Configuration/Migrations/20250828064850_RenameApiParametersToParameters.Designer.cs b/ConduitLLM.Configuration/Migrations/20250828064850_RenameApiParametersToParameters.Designer.cs deleted file mode 100644 index e7c98576a..000000000 --- a/ConduitLLM.Configuration/Migrations/20250828064850_RenameApiParametersToParameters.Designer.cs +++ /dev/null @@ -1,2265 +0,0 @@ -// -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("20250828064850_RenameApiParametersToParameters")] - partial class RenameApiParametersToParameters - { - /// - 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/20250828064850_RenameApiParametersToParameters.cs b/ConduitLLM.Configuration/Migrations/20250828064850_RenameApiParametersToParameters.cs deleted file mode 100644 index 820291382..000000000 --- a/ConduitLLM.Configuration/Migrations/20250828064850_RenameApiParametersToParameters.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ConduitLLM.Configuration.Migrations -{ - /// - public partial class RenameApiParametersToParameters : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "ApiParameters", - table: "Models", - newName: "Parameters"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "Parameters", - table: "Models", - newName: "ApiParameters"); - } - } -} 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 deleted file mode 100644 index dc45b0708..000000000 --- a/ConduitLLM.Configuration/ProviderDefaultModels.cs +++ /dev/null @@ -1,103 +0,0 @@ -namespace ConduitLLM.Configuration; - -/// -/// Defines default model configurations for different providers and operation types. -/// This replaces hardcoded model defaults throughout the codebase. -/// -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. - /// - 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. -/// -public class ProviderSpecificDefaults -{ - /// - /// Gets or sets the default model for chat completions. - /// - public string? DefaultChatModel { get; set; } - - /// - /// Gets or sets the default model for embeddings. - /// - public string? DefaultEmbeddingModel { get; set; } - - /// - /// Gets or sets the default model for image generation. - /// - public string? DefaultImageModel { get; set; } - - /// - /// Gets or sets model aliases for this provider. - /// Maps user-friendly names to provider-specific model IDs. - /// - public Dictionary ModelAliases { get; set; } = new(); -} diff --git a/ConduitLLM.Configuration/README.md b/ConduitLLM.Configuration/README.md deleted file mode 100644 index fe2b94146..000000000 --- a/ConduitLLM.Configuration/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# ConduitLLM.Configuration - -## Overview - -**ConduitLLM.Configuration** is a core library within the [Conduit.sln](../Conduit.sln) solution. Its primary role is to centralize and standardize configuration logic, settings, and extension methods that are shared across the various ConduitLLM projects, such as the API server, WebUI, and background services. - -By encapsulating configuration concerns in a single project, ConduitLLM.Configuration ensures consistency, maintainability, and ease of deployment for the entire ConduitLLM ecosystem. - ---- - -## How It Fits Into the Conduit Solution - -The Conduit solution is a modular, scalable platform for large language model (LLM) orchestration, featuring components for API serving, web-based UI, caching, and more. `ConduitLLM.Configuration` is referenced by other projects in the solution to: - -- Provide strongly-typed configuration objects (e.g., for caching, LLM providers, API keys, service endpoints). -- Register and configure services via extension methods (e.g., for dependency injection). -- Centralize environment variable and appsettings management. -- Ensure all ConduitLLM components follow the same configuration patterns. - ---- - -## Key Features - -- **Extension Methods:** Utilities for registering and configuring services (e.g., caching, LLM clients) in ASP.NET Core dependency injection. -- **Strongly-Typed Settings:** Classes that map to configuration sections (from `appsettings.json` or environment variables) for safe, discoverable access. -- **Environment Variable Support:** Reads and applies configuration from environment variables, supporting containerized and cloud deployments. -- **Shared Conventions:** Enforces consistent configuration keys, section names, and patterns across the solution. - ---- - -## Usage - -### Referencing in Other Projects - -Add a project reference to `ConduitLLM.Configuration` from any ConduitLLM project that requires shared configuration logic. - -```xml - - - -``` - -### Registering Configuration in ASP.NET Core - -In your `Program.cs` or `Startup.cs`, use the provided extension methods to register configuration and services. For example: - -```csharp -using ConduitLLM.Configuration.Extensions; - -var builder = WebApplication.CreateBuilder(args); - -// Register LLM caching with configuration -builder.Services.AddLLMCaching(builder.Configuration); - -// Register other shared services -builder.Services.AddConduitLLMConfiguration(builder.Configuration); -``` - -### Configuration Sources - -- **appsettings.json:** Place configuration sections here for local development. -- **Environment Variables:** Override or provide configuration in production or containerized environments. - -#### Example: Environment Variable Usage - -```bash -export LLM__Provider=OpenAI -export LLM__ApiKey=your-api-key -export Cache__Enabled=true -``` - ---- - -## Configuration Sections - -Common configuration sections managed by this project include (but are not limited to): - -- `LLM`: Settings for LLM provider, API keys, model selection, etc. -- `Cache`: Caching options and backend configuration. -- `ServiceEndpoints`: URLs and ports for internal/external services. - ---- - -## Best Practices - -- **Single Source of Truth:** Always define shared configuration logic in this project to avoid duplication. -- **Use Strongly-Typed Classes:** Access configuration via injected options classes for safety and discoverability. -- **Environment Overrides:** Prefer environment variables for secrets and deployment-specific settings. - ---- - -## Contributing - -If you are adding new configuration logic or extension methods, ensure they are generic, reusable, and well-documented. Update this README as needed. - ---- - -## License - -This project is part of the ConduitLLM solution and inherits its license. 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/IModelCapabilitiesRepository.cs b/ConduitLLM.Configuration/Repositories/IModelCapabilitiesRepository.cs deleted file mode 100644 index 506ed8b99..000000000 --- a/ConduitLLM.Configuration/Repositories/IModelCapabilitiesRepository.cs +++ /dev/null @@ -1,40 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Configuration.Repositories -{ - /// - /// Repository interface for ModelCapabilities entity operations. - /// - public interface IModelCapabilitiesRepository - { - /// - /// Gets model capabilities by its ID. - /// - Task GetByIdAsync(int id); - - /// - /// Gets all model capabilities. - /// - Task> GetAllAsync(); - - /// - /// Gets models using specific capabilities. - /// - Task?> GetModelsUsingCapabilitiesAsync(int capabilitiesId); - - /// - /// Creates new model capabilities. - /// - Task CreateAsync(ModelCapabilities capabilities); - - /// - /// Updates existing model capabilities. - /// - Task UpdateAsync(ModelCapabilities capabilities); - - /// - /// Deletes model capabilities by ID. - /// - Task DeleteAsync(int id); - } -} \ No newline at end of file diff --git a/ConduitLLM.Configuration/Repositories/ModelCapabilitiesRepository.cs b/ConduitLLM.Configuration/Repositories/ModelCapabilitiesRepository.cs deleted file mode 100644 index 3a597d02d..000000000 --- a/ConduitLLM.Configuration/Repositories/ModelCapabilitiesRepository.cs +++ /dev/null @@ -1,79 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -using Microsoft.EntityFrameworkCore; - -namespace ConduitLLM.Configuration.Repositories -{ - /// - /// Repository for ModelCapabilities entity operations. - /// - public class ModelCapabilitiesRepository : IModelCapabilitiesRepository - { - private readonly IDbContextFactory _dbContextFactory; - - public ModelCapabilitiesRepository(IDbContextFactory dbContextFactory) - { - _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); - } - - public async Task GetByIdAsync(int id) - { - using var context = await _dbContextFactory.CreateDbContextAsync(); - return await context.Set() - .FirstOrDefaultAsync(c => c.Id == id); - } - - public async Task> GetAllAsync() - { - using var context = await _dbContextFactory.CreateDbContextAsync(); - return await context.Set() - .OrderBy(c => c.Id) - .ToListAsync(); - } - - public async Task?> GetModelsUsingCapabilitiesAsync(int capabilitiesId) - { - using var context = await _dbContextFactory.CreateDbContextAsync(); - var exists = await context.Set() - .AnyAsync(c => c.Id == capabilitiesId); - - if (!exists) - return null; - - return await context.Set() - .Where(m => m.ModelCapabilitiesId == capabilitiesId) - .OrderBy(m => m.Name) - .ToListAsync(); - } - - public async Task CreateAsync(ModelCapabilities capabilities) - { - using var context = await _dbContextFactory.CreateDbContextAsync(); - context.Set().Add(capabilities); - await context.SaveChangesAsync(); - return capabilities; - } - - public async Task UpdateAsync(ModelCapabilities capabilities) - { - using var context = await _dbContextFactory.CreateDbContextAsync(); - context.Set().Update(capabilities); - await context.SaveChangesAsync(); - return capabilities; - } - - public async Task DeleteAsync(int id) - { - using var context = await _dbContextFactory.CreateDbContextAsync(); - var capabilities = await context.Set() - .FirstOrDefaultAsync(c => c.Id == id); - - if (capabilities == null) - return false; - - context.Set().Remove(capabilities); - await context.SaveChangesAsync(); - return true; - } - } -} \ No newline at end of file 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/README.md b/ConduitLLM.Configuration/Repositories/README.md deleted file mode 100644 index ff968e029..000000000 --- a/ConduitLLM.Configuration/Repositories/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# Repository Pattern Implementation for Conduit - -This directory contains interfaces and implementations for the Repository pattern, which provides an abstraction layer between the domain model and the data access layer. - -## Implementation Strategy - -The Repository pattern is being implemented gradually in the Conduit codebase. The initial focus is on the `VirtualKeyRepository` as a proof of concept. Once this is working correctly, the pattern will be extended to other entities. - -## Key Benefits - -1. **Separation of Concerns**: Data access logic is separated from business logic -2. **Testability**: Services are easier to test as database access can be mocked -3. **Maintainability**: Database access code is centralized and follows a consistent pattern -4. **Flexibility**: The pattern makes it easier to transition between direct database access and API calls - -## Current Implementation Status - -- ✅ `IVirtualKeyRepository` and `VirtualKeyRepository` - Complete -- ⏳ `IGlobalSettingRepository` and `GlobalSettingRepository` - In progress -- ❌ Other repositories - Planned - -## Usage Example - -```csharp -// In services -public class VirtualKeyService -{ - private readonly IVirtualKeyRepository _repository; - - public VirtualKeyService(IVirtualKeyRepository repository) - { - _repository = repository; - } - - public async Task ResetSpendAsync(int id) - { - var virtualKey = await _repository.GetByIdAsync(id); - if (virtualKey == null) return false; - - virtualKey.CurrentSpend = 0; - virtualKey.BudgetStartDate = DetermineBudgetStartDate(virtualKey.BudgetDuration); - virtualKey.UpdatedAt = DateTime.UtcNow; - - return await _repository.UpdateAsync(virtualKey); - } -} - -// In controllers, you can use either direct DB access (WebUI admin pages) -// or API calls (external applications) without changing service logic -``` - -## Next Steps - -1. Complete the implementation of the VirtualKeyRepository -2. Add unit tests for the repository -3. Refactor the VirtualKeyService to use the repository -4. Gradually implement repositories for other entities \ No newline at end of file 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.Configuration/Services/CacheService.cs b/ConduitLLM.Configuration/Services/CacheService.cs deleted file mode 100644 index 651a3994e..000000000 --- a/ConduitLLM.Configuration/Services/CacheService.cs +++ /dev/null @@ -1,112 +0,0 @@ -using ConduitLLM.Configuration.Options; - -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Configuration.Services -{ - /// - /// Generic caching service for the application - /// - public class CacheService : ICacheService - { - private readonly IMemoryCache _memoryCache; - private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - private readonly CacheOptions _cacheOptions; - - public CacheService(IMemoryCache memoryCache, IOptions cacheOptions) - { - _memoryCache = memoryCache; - _cacheOptions = cacheOptions.Value; - } - - /// - public T? Get(string key) - { - _memoryCache.TryGetValue(key, out T? value); - return value; - } - - /// - public void Set(string key, T value, TimeSpan? absoluteExpiration = null, TimeSpan? slidingExpiration = null) - { - var options = new MemoryCacheEntryOptions(); - - // Use provided expiration time or fall back to default from configuration - var absExpiration = absoluteExpiration ?? _cacheOptions.DefaultAbsoluteExpiration; - var slideExpiration = slidingExpiration ?? _cacheOptions.DefaultSlidingExpiration; - - if (absExpiration.HasValue) - options.AbsoluteExpirationRelativeToNow = absExpiration.Value; - - if (slideExpiration.HasValue) - options.SlidingExpiration = slideExpiration.Value; - - _memoryCache.Set(key, value, options); - } - - /// - public void Remove(string key) - { - _memoryCache.Remove(key); - } - - /// - public async Task GetOrCreateAsync(string key, Func> factory, TimeSpan? absoluteExpiration = null, TimeSpan? slidingExpiration = null) - { - // Check if the item exists in the cache - if (_memoryCache.TryGetValue(key, out T? cachedValue) && cachedValue != null) - { - return cachedValue; - } - - // Use semaphore to prevent multiple simultaneous initializations - await _semaphore.WaitAsync(); - try - { - // Check again after acquiring the lock - if (_memoryCache.TryGetValue(key, out T? lockCachedValue) && lockCachedValue != null) - { - return lockCachedValue; - } - - // Item is not in the cache, create it - var newValue = await factory(); - - // Set cache options - var options = new MemoryCacheEntryOptions(); - - // Use provided expiration time or fall back to default from configuration - var absExpiration = absoluteExpiration ?? _cacheOptions.DefaultAbsoluteExpiration; - var slideExpiration = slidingExpiration ?? _cacheOptions.DefaultSlidingExpiration; - - if (absExpiration.HasValue) - options.AbsoluteExpirationRelativeToNow = absExpiration.Value; - - if (slideExpiration.HasValue) - options.SlidingExpiration = slideExpiration.Value; - - // Save to cache and return - _memoryCache.Set(key, newValue, options); - return newValue; - } - finally - { - _semaphore.Release(); - } - } - - /// - public void RemoveByPrefix(string prefix) - { - // Since MemoryCache doesn't support native prefix-based removal, - // we would need a separate cache key tracking mechanism. - // For simplicity, this is a basic implementation - // In a production app, you might use a more sophisticated approach - - // Not implemented in this basic version - // Would require tracking cache keys with a given prefix - } - } -} diff --git a/ConduitLLM.Configuration/Services/CacheServiceFactory.cs b/ConduitLLM.Configuration/Services/CacheServiceFactory.cs deleted file mode 100644 index 8864fefd6..000000000 --- a/ConduitLLM.Configuration/Services/CacheServiceFactory.cs +++ /dev/null @@ -1,149 +0,0 @@ -using ConduitLLM.Configuration.Interfaces; -using ConduitLLM.Configuration.Options; - -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace ConduitLLM.Configuration.Services -{ - /// - /// Factory that creates the appropriate cache service based on configuration - /// - /// - /// This factory creates either a memory-based or Redis-based cache service - /// depending on the configuration. It encapsulates the creation logic and dependency - /// injection for the different cache implementations. - /// - public class CacheServiceFactory - { - private readonly IOptions _cacheOptions; - private readonly IMemoryCache _memoryCache; - private readonly IDistributedCache _distributedCache; - private readonly ILoggerFactory _loggerFactory; - private readonly RedisConnectionFactory _redisConnectionFactory; - - /// - /// Creates a new instance of CacheServiceFactory - /// - /// Cache configuration options - /// Memory cache instance - /// Distributed cache instance - /// Logger factory - /// Redis connection factory - public CacheServiceFactory( - IOptions cacheOptions, - IMemoryCache memoryCache, - IDistributedCache distributedCache, - ILoggerFactory loggerFactory, - RedisConnectionFactory redisConnectionFactory) - { - _cacheOptions = cacheOptions ?? throw new ArgumentNullException(nameof(cacheOptions)); - _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); - _distributedCache = distributedCache ?? throw new ArgumentNullException(nameof(distributedCache)); - _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - _redisConnectionFactory = redisConnectionFactory ?? throw new ArgumentNullException(nameof(redisConnectionFactory)); - } - - /// - /// Creates the appropriate cache service based on configuration - /// - /// An implementation of ICacheService - public async Task CreateCacheServiceAsync() - { - var options = _cacheOptions.Value; - - // If cache is disabled, return a null cache implementation - if (!options.IsEnabled) - { - return new NullCacheService(_loggerFactory.CreateLogger()); - } - - // Based on the cache type, create the appropriate service - switch (options.CacheType?.ToLowerInvariant()) - { - case "redis": - try - { - var connection = await _redisConnectionFactory.GetConnectionAsync(); - return new RedisCacheService( - _distributedCache, - connection, - _cacheOptions, - _loggerFactory.CreateLogger()); - } - catch (Exception ex) - { - // Log the error and fall back to memory cache - var logger = _loggerFactory.CreateLogger(); - logger.LogError(ex, "Failed to create Redis cache service, falling back to memory cache"); - return CreateMemoryCacheService(); - } - - case "memory": - default: - return CreateMemoryCacheService(); - } - } - - private CacheService CreateMemoryCacheService() - { - return new CacheService( - _memoryCache, - _cacheOptions); - } - } - - /// - /// A no-op implementation of ICacheService that doesn't cache anything - /// - /// - /// This implementation is used when caching is disabled. - /// All operations are no-ops and simply pass through to the factory method. - /// - public class NullCacheService : ICacheService - { - private readonly ILogger _logger; - - /// - /// Creates a new instance of NullCacheService - /// - /// Logger instance - public NullCacheService(ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _logger.LogInformation("Using null cache service - caching is disabled"); - } - - /// - public T? Get(string key) - { - return default; - } - - /// - public async Task GetOrCreateAsync(string key, Func> factory, TimeSpan? absoluteExpiration = null, TimeSpan? slidingExpiration = null) - { - return await factory(); - } - - /// - public void Remove(string key) - { - // No-op - } - - /// - public void RemoveByPrefix(string prefix) - { - // No-op - } - - /// - public void Set(string key, T value, TimeSpan? absoluteExpiration = null, TimeSpan? slidingExpiration = null) - { - // No-op - } - } -} diff --git a/ConduitLLM.Configuration/Services/ModelCostService.cs b/ConduitLLM.Configuration/Services/ModelCostService.cs deleted file mode 100644 index ea3edc433..000000000 --- a/ConduitLLM.Configuration/Services/ModelCostService.cs +++ /dev/null @@ -1,235 +0,0 @@ -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.Interfaces; - -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Configuration.Services; - -/// -/// Service for managing and retrieving model costs, with caching support -/// -public class ModelCostService : IModelCostService -{ - private readonly IModelCostRepository _modelCostRepository; - private readonly IModelProviderMappingRepository _modelProviderMappingRepository; - private readonly IMemoryCache _cache; - private readonly ILogger _logger; - private readonly TimeSpan _cacheDuration = TimeSpan.FromHours(1); - private const string CacheKeyPrefix = "ModelCost_"; - private const string AllModelsCacheKey = CacheKeyPrefix + "All"; - - /// - /// Creates a new instance of the ModelCostService - /// - /// The model cost repository - /// The model provider mapping repository - /// The memory cache - /// The logger - public ModelCostService( - IModelCostRepository modelCostRepository, - IModelProviderMappingRepository modelProviderMappingRepository, - IMemoryCache cache, - ILogger logger) - { - _modelCostRepository = modelCostRepository ?? throw new ArgumentNullException(nameof(modelCostRepository)); - _modelProviderMappingRepository = modelProviderMappingRepository ?? throw new ArgumentNullException(nameof(modelProviderMappingRepository)); - _cache = cache ?? throw new ArgumentNullException(nameof(cache)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public async Task GetCostForModelAsync(string modelId, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(modelId)) - { - throw new ArgumentException("Model ID cannot be empty", nameof(modelId)); - } - - try - { - string cacheKey = $"{CacheKeyPrefix}{modelId}"; - - // Try to get from cache first - if (_cache.TryGetValue(cacheKey, out ModelCost? cachedCost)) - { - _logger.LogDebug("Cache hit for model cost: {ModelId}", modelId); - return cachedCost; - } - - _logger.LogDebug("Cache miss for model cost: {ModelId}, querying database", modelId); - - // Find the ModelProviderMapping by alias - var modelMapping = await _modelProviderMappingRepository.GetByModelNameAsync(modelId, cancellationToken); - - if (modelMapping == null) - { - _logger.LogDebug("No model provider mapping found for alias: {ModelId}", modelId); - _cache.Set(cacheKey, null, _cacheDuration); - return null; - } - - // Get the associated cost through the junction table - // First, get all model costs to include the navigation properties - var allCosts = await _modelCostRepository.GetAllAsync(cancellationToken); - - // Find the cost associated with this model mapping - var modelCost = allCosts.FirstOrDefault(cost => - cost.ModelCostMappings.Any(mapping => - mapping.ModelProviderMappingId == modelMapping.Id && - mapping.IsActive)); - - // If we found multiple costs, prioritize by: - // 1. Active status - // 2. Effective date (most recent that's not in the future) - // 3. Priority - if (modelCost == null) - { - var now = DateTime.UtcNow; - var candidateCosts = allCosts - .Where(cost => cost.ModelCostMappings.Any(mapping => - mapping.ModelProviderMappingId == modelMapping.Id && - mapping.IsActive)) - .Where(cost => cost.IsActive && cost.EffectiveDate <= now) - .Where(cost => !cost.ExpiryDate.HasValue || cost.ExpiryDate.Value > now) - .OrderByDescending(cost => cost.Priority) - .ThenByDescending(cost => cost.EffectiveDate); - - modelCost = candidateCosts.FirstOrDefault(); - } - - _cache.Set(cacheKey, modelCost, _cacheDuration); - return modelCost; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting cost for model {ModelId}", modelId); - throw; - } - } - - /// - public async Task> ListModelCostsAsync(CancellationToken cancellationToken = default) - { - try - { - // Try to get from cache first - if (_cache.TryGetValue(AllModelsCacheKey, out List? cachedCosts) && cachedCosts != null) - { - return cachedCosts; - } - - var costs = await _modelCostRepository.GetAllAsync(cancellationToken); - - _cache.Set(AllModelsCacheKey, costs, _cacheDuration); - return costs; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error listing model costs"); - throw; - } - } - - /// - public async Task AddModelCostAsync(ModelCost modelCost, CancellationToken cancellationToken = default) - { - if (modelCost == null) - { - throw new ArgumentNullException(nameof(modelCost)); - } - - try - { - modelCost.CreatedAt = DateTime.UtcNow; - modelCost.UpdatedAt = DateTime.UtcNow; - - await _modelCostRepository.CreateAsync(modelCost, cancellationToken); - - // Clear cache - ClearCache(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error adding model cost {CostName}", modelCost.CostName); - throw; - } - } - - /// - public async Task UpdateModelCostAsync(ModelCost modelCost, CancellationToken cancellationToken = default) - { - if (modelCost == null) - { - throw new ArgumentNullException(nameof(modelCost)); - } - - try - { - var existingCost = await _modelCostRepository.GetByIdAsync(modelCost.Id, cancellationToken); - - if (existingCost == null) - { - return false; - } - - // Update properties - existingCost.CostName = modelCost.CostName; - existingCost.InputCostPerMillionTokens = modelCost.InputCostPerMillionTokens; - existingCost.OutputCostPerMillionTokens = modelCost.OutputCostPerMillionTokens; - existingCost.EmbeddingCostPerMillionTokens = modelCost.EmbeddingCostPerMillionTokens; - existingCost.CachedInputCostPerMillionTokens = modelCost.CachedInputCostPerMillionTokens; - existingCost.CachedInputWriteCostPerMillionTokens = modelCost.CachedInputWriteCostPerMillionTokens; - existingCost.ImageCostPerImage = modelCost.ImageCostPerImage; - existingCost.UpdatedAt = DateTime.UtcNow; - - bool result = await _modelCostRepository.UpdateAsync(existingCost, cancellationToken); - - // Clear cache - ClearCache(); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating model cost with ID {ModelCostId}", modelCost.Id); - throw; - } - } - - /// - public async Task DeleteModelCostAsync(int id, CancellationToken cancellationToken = default) - { - try - { - bool result = await _modelCostRepository.DeleteAsync(id, cancellationToken); - - if (result) - { - // Clear cache - ClearCache(); - } - - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting model cost with ID {ModelCostId}", id); - throw; - } - } - - /// - public void ClearCache() - { - // Remove all ModelCost-related entries from the cache - // This is a simple approach; in a production environment with many entries, - // you might want to use a more sophisticated cache invalidation strategy - _logger.LogInformation("Clearing model cost cache"); - - // First, remove the 'all models' cache - _cache.Remove(AllModelsCacheKey); - - // For a distributed cache system, you might need a more advanced approach - // to track and remove all cache keys, potentially using a separate list of keys - } -} diff --git a/ConduitLLM.Configuration/Services/RedisCacheService.cs b/ConduitLLM.Configuration/Services/RedisCacheService.cs deleted file mode 100644 index b6d2fb487..000000000 --- a/ConduitLLM.Configuration/Services/RedisCacheService.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System.Text.Json; - -using ConduitLLM.Configuration.Options; - -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using StackExchange.Redis; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Configuration.Services -{ - /// - /// Redis implementation of the cache service - /// - /// - /// This implementation uses StackExchange.Redis and IDistributedCache for Redis caching. - /// It provides thread-safe operations and implements all ICacheService methods. - /// - public class RedisCacheService : ICacheService, IDisposable - { - private readonly IDistributedCache _distributedCache; - private readonly IConnectionMultiplexer _connectionMultiplexer; - private readonly IDatabase _redisDatabase; - private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - private readonly CacheOptions _cacheOptions; - private readonly ILogger _logger; - private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }; - - /// - /// Creates a new instance of RedisCacheService - /// - /// The distributed cache implementation - /// The Redis connection multiplexer - /// Cache configuration options - /// Logger instance - public RedisCacheService( - IDistributedCache distributedCache, - IConnectionMultiplexer connectionMultiplexer, - IOptions cacheOptions, - ILogger logger) - { - _distributedCache = distributedCache ?? throw new ArgumentNullException(nameof(distributedCache)); - _connectionMultiplexer = connectionMultiplexer ?? throw new ArgumentNullException(nameof(connectionMultiplexer)); - _redisDatabase = _connectionMultiplexer.GetDatabase(); - _cacheOptions = cacheOptions?.Value ?? throw new ArgumentNullException(nameof(cacheOptions)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public T? Get(string key) - { - try - { - var value = _distributedCache.GetString(key); - if (string.IsNullOrEmpty(value)) - { - return default; - } - - return JsonSerializer.Deserialize(value, _jsonOptions); - } - catch (Exception ex) - { -_logger.LogError(ex, "Error retrieving item from Redis cache with key {Key}".Replace(Environment.NewLine, ""), key.Replace(Environment.NewLine, "")); - return default; - } - } - - /// - public void Set(string key, T value, TimeSpan? absoluteExpiration = null, TimeSpan? slidingExpiration = null) - { - try - { - var options = new DistributedCacheEntryOptions(); - - // Use provided expiration time or fall back to default from configuration - var absExpiration = absoluteExpiration ?? _cacheOptions.DefaultAbsoluteExpiration; - var slideExpiration = slidingExpiration ?? _cacheOptions.DefaultSlidingExpiration; - - if (absExpiration.HasValue) - options.AbsoluteExpirationRelativeToNow = absExpiration.Value; - - if (slideExpiration.HasValue) - options.SlidingExpiration = slideExpiration.Value; - - var serializedValue = JsonSerializer.Serialize(value, _jsonOptions); - _distributedCache.SetString(key, serializedValue, options); - } - catch (Exception ex) - { -_logger.LogError(ex, "Error setting item in Redis cache with key {Key}".Replace(Environment.NewLine, ""), key.Replace(Environment.NewLine, "")); - } - } - - /// - public void Remove(string key) - { - try - { - _distributedCache.Remove(key); - } - catch (Exception ex) - { -_logger.LogError(ex, "Error removing item from Redis cache with key {Key}".Replace(Environment.NewLine, ""), key.Replace(Environment.NewLine, "")); - } - } - - /// - public async Task GetOrCreateAsync(string key, Func> factory, TimeSpan? absoluteExpiration = null, TimeSpan? slidingExpiration = null) - { - // Check if the item exists in the cache - var cachedValue = Get(key); - if (cachedValue != null) - { - return cachedValue; - } - - // Use semaphore to prevent multiple simultaneous initializations - await _semaphore.WaitAsync(); - try - { - // Check again after acquiring the lock - cachedValue = Get(key); - if (cachedValue != null) - { - return cachedValue; - } - - // Item is not in the cache, create it - var newValue = await factory(); - - // Save to cache and return - Set(key, newValue, absoluteExpiration, slidingExpiration); - return newValue; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in GetOrCreateAsync for key {Key}", key); - // If there's an error, try to execute the factory directly - try - { - return await factory(); - } - catch - { - // If the factory also fails, re-throw the original exception - throw; - } - } - finally - { - _semaphore.Release(); - } - } - - /// - public void RemoveByPrefix(string prefix) - { - try - { - // Redis supports pattern matching for key deletion - // This will find all keys that match the specified pattern - var server = _connectionMultiplexer.GetServer(_connectionMultiplexer.GetEndPoints()[0]); - var keys = server.Keys(pattern: $"{prefix}*"); - - foreach (var key in keys) - { - _redisDatabase.KeyDelete(key); - } - - _logger.LogInformation("Removed all items with prefix {Prefix} from Redis cache", prefix); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error removing items by prefix {Prefix} from Redis cache", prefix); - } - } - - /// - /// Disposes of resources used by the service - /// - public void Dispose() - { - _semaphore.Dispose(); - } - } -} diff --git a/ConduitLLM.Core/Conduit.cs b/ConduitLLM.Core/Conduit.cs deleted file mode 100644 index 6b6fc2f61..000000000 --- a/ConduitLLM.Core/Conduit.cs +++ /dev/null @@ -1,361 +0,0 @@ -using ConduitLLM.Core.Configuration; -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Core -{ - /// - /// Main entry point for interacting with the ConduitLLM library. - /// Orchestrates calls to different LLM providers based on configuration via an . - /// - public class Conduit : IConduit - { - private readonly ILLMClientFactory _clientFactory; - private readonly ILLMRouter? _router; - private readonly IContextManager? _contextManager; - private readonly IModelProviderMappingService? _modelProviderMappingService; - private readonly IOptions? _contextOptions; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// 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. - /// Thrown if clientFactory is null. - 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; - } - - /// - /// Creates a chat completion using the configured LLM providers. - /// - /// The chat completion request, including the target model alias. - /// Optional API key to override the configured key for this request. - /// A token to cancel the operation. - /// The chat completion response from the selected LLM provider. - /// Thrown if the request is null. - /// Thrown if the request.Model is null or whitespace. - /// Thrown if configuration for the requested model is invalid or missing. - /// Thrown if the provider for the requested model is not supported. - /// Thrown if communication with the LLM provider fails. - public async Task CreateChatCompletionAsync( - ChatCompletionRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(request); - if (string.IsNullOrWhiteSpace(request.Model)) - { - throw new ArgumentException("The request must specify a target Model alias.", "request.Model"); - } - - // 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); - - // 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); - } - } - - /// - /// Creates a streaming chat completion using the configured LLM providers. - /// - /// The chat completion request, including the target model alias. - /// Optional API key to override the configured key for this request. - /// A token to cancel the operation. - /// An asynchronous enumerable of chat completion chunks from the selected LLM provider. - /// Thrown if the request is null. - /// Thrown if the request.Model is null or whitespace. - /// Thrown if configuration for the requested model is invalid or missing. - /// Thrown if the provider for the requested model is not supported. - /// Thrown if communication with the LLM provider fails during streaming. - public async IAsyncEnumerable StreamChatCompletionAsync( - ChatCompletionRequest request, - string? apiKey = null, - [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(request); - if (string.IsNullOrWhiteSpace(request.Model)) - { - throw new ArgumentException("The request must specify a target Model alias.", "request.Model"); - } - - // 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 streaming request - await foreach (var chunk in _router.StreamChatCompletionAsync(request, routingStrategy, apiKey, cancellationToken)) - { - yield return chunk; - } - } - 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); - - // 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; - } - } - } - - /// - /// Applies context window management to trim message history if needed. - /// - /// The original chat completion request - /// The request with potentially trimmed messages - private async Task ApplyContextManagementAsync(ChatCompletionRequest request) - { - // Skip if context management is disabled or services aren't available - if (_contextManager == null || _modelProviderMappingService == null || _contextOptions == null || - !_contextOptions.Value.EnableAutomaticContextManagement) - { - return request; - } - - try - { - // Get model context window limit - int? maxContextTokens = null; - - // First try to get model-specific context limit - var mapping = await _modelProviderMappingService.GetMappingByModelAliasAsync(request.Model); - - // Handle the MaxContextTokens property which may or may not exist yet - // depending on whether the migration has been applied - var maxContextTokensProperty = mapping?.GetType().GetProperty("MaxContextTokens"); - if (mapping != null && maxContextTokensProperty != null) - { - maxContextTokens = maxContextTokensProperty.GetValue(mapping) as int?; - if (maxContextTokens.HasValue) - { - _logger.LogDebug("Using model-specific context window limit of {Tokens} tokens for {Model}", - maxContextTokens, request.Model); - } - } - - // Fall back to default limit if configured - if (!maxContextTokens.HasValue && _contextOptions.Value.DefaultMaxContextTokens.HasValue) - { - maxContextTokens = _contextOptions.Value.DefaultMaxContextTokens; - _logger.LogDebug("Using default context window limit of {Tokens} tokens for {Model}", - maxContextTokens, request.Model); - } - - // Apply context management if we have a limit - if (maxContextTokens.HasValue && _contextManager != null) - { - return await _contextManager.ManageContextAsync(request, maxContextTokens.Value); - } - } - catch (Exception ex) - { - // Log error but don't fail the request - just pass through without context management - _logger.LogError(ex, "Error applying context management for model {Model}", request.Model); - } - - return request; - } - - /// - /// Creates an embedding using the configured LLM providers. - /// - /// The embedding request, including the target model alias. - /// Optional API key to override the configured key for this request. - /// A token to cancel the operation. - /// The embedding response from the selected LLM provider. - /// Thrown if the request is null. - /// Thrown if the request.Model is null or whitespace. - /// Thrown if configuration for the requested model is invalid or missing. - /// Thrown if the provider for the requested model is not supported. - /// Thrown if communication with the LLM provider fails. - public async Task CreateEmbeddingAsync( - EmbeddingRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(request); - if (string.IsNullOrWhiteSpace(request.Model)) - throw new ArgumentException("The request must specify a target Model alias.", "request.Model"); - - // No router for embeddings (OpenAI spec does not support routing for embeddings) - ILLMClient client = _clientFactory.GetClient(request.Model); - return await client.CreateEmbeddingAsync(request, apiKey, cancellationToken).ConfigureAwait(false); - } - - /// - /// Creates an image generation using the configured LLM providers. - /// - /// The image generation request, including the target model alias. - /// Optional API key to override the configured key for this request. - /// A token to cancel the operation. - /// The image generation response from the selected LLM provider. - /// Thrown if the request is null. - /// Thrown if the request.Model is null or whitespace. - /// Thrown if configuration for the requested model is invalid or missing. - /// Thrown if the provider for the requested model is not supported. - /// Thrown if communication with the LLM provider fails. - public async Task CreateImageAsync( - ImageGenerationRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(request); - if (string.IsNullOrWhiteSpace(request.Model)) - throw new ArgumentException("The request must specify a target Model alias.", "request.Model"); - - // No router for image generation (OpenAI spec does not support routing for images) - ILLMClient client = _clientFactory.GetClient(request.Model); - 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. - /// - /// The model alias to get a client for. - /// The LLM client for the specified model. - public ILLMClient GetClient(string modelAlias) - { - return _clientFactory.GetClient(modelAlias); - } - - // Add other high-level methods as needed. - } -} diff --git a/ConduitLLM.Core/ConduitLLM.Core.csproj b/ConduitLLM.Core/ConduitLLM.Core.csproj deleted file mode 100644 index 3fbee5541..000000000 --- a/ConduitLLM.Core/ConduitLLM.Core.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - net9.0 - enable - enable - - - diff --git a/ConduitLLM.Core/Configuration/ContextManagementOptions.cs b/ConduitLLM.Core/Configuration/ContextManagementOptions.cs deleted file mode 100644 index 275de775a..000000000 --- a/ConduitLLM.Core/Configuration/ContextManagementOptions.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace ConduitLLM.Core.Configuration -{ - /// - /// Options for context window management in LLM requests. - /// - public class ContextManagementOptions - { - /// - /// Gets or sets a value indicating whether automatic context window management is enabled. - /// When enabled, the system will automatically trim conversation history to fit within model context limits. - /// - public bool EnableAutomaticContextManagement { get; set; } = true; - - /// - /// Gets or sets the default maximum context window size in tokens. - /// This is used as a fallback when a model-specific limit is not configured. - /// - public int? DefaultMaxContextTokens { get; set; } = 4000; - } -} diff --git a/ConduitLLM.Core/Events/DomainEvents.ModelConfiguration.cs b/ConduitLLM.Core/Events/DomainEvents.ModelConfiguration.cs deleted file mode 100644 index 7f9a329c0..000000000 --- a/ConduitLLM.Core/Events/DomainEvents.ModelConfiguration.cs +++ /dev/null @@ -1,167 +0,0 @@ -namespace ConduitLLM.Core.Events -{ - - // =============================== - // Model Cost Domain Events - // =============================== - - /// - /// Raised when model costs are created, updated, or deleted - /// Critical for cache invalidation across all services - /// - public record ModelCostChanged : DomainEvent - { - /// - /// Model cost database ID - /// - public int ModelCostId { get; init; } - - /// - /// Cost name that was affected - /// - public string CostName { get; init; } = string.Empty; - - /// - /// Type of change (Created, Updated, Deleted) - /// - public string ChangeType { get; init; } = string.Empty; - - /// - /// Properties that were changed (for selective invalidation) - /// - public string[] ChangedProperties { get; init; } = Array.Empty(); - - /// - /// Partition key for ordered processing per model cost - /// - public string PartitionKey => ModelCostId.ToString(); - } - - // =============================== - // Global Setting Domain Events - // =============================== - - /// - /// Raised when a global setting is created, updated, or deleted - /// Critical for cache invalidation across all services - /// - public record GlobalSettingChanged : DomainEvent - { - /// - /// Global setting database ID - /// - public int SettingId { get; init; } - - /// - /// Global setting key - /// - public string SettingKey { get; init; } = string.Empty; - - /// - /// Type of change (Created, Updated, Deleted) - /// - public string ChangeType { get; init; } = string.Empty; - - /// - /// Properties that were changed (for selective invalidation) - /// - public string[] ChangedProperties { get; init; } = Array.Empty(); - - /// - /// Partition key for ordered processing per setting - /// - public string PartitionKey => SettingId.ToString(); - } - - // =============================== - // Model Mapping Domain Events - // =============================== - - /// - /// Raised when model mappings are added or updated - /// Critical for navigation state updates - /// - public record ModelMappingChanged : DomainEvent - { - /// - /// Model mapping database ID - /// - public int MappingId { get; init; } - - /// - /// Model alias - /// - public string ModelAlias { get; init; } = string.Empty; - - /// - /// Provider credential ID - /// - public int ProviderId { get; init; } - - /// - /// Whether the mapping is enabled - /// - public bool IsEnabled { get; init; } - - /// - /// Type of change (Created, Updated, Deleted) - /// - public string ChangeType { get; init; } = string.Empty; - - /// - /// Partition key for ordered processing per mapping - /// - public string PartitionKey => MappingId.ToString(); - } - - // =============================== - // IP Filter Domain Events - // =============================== - - /// - /// Raised when an IP filter is created, updated, or deleted - /// Critical for cache invalidation and security policy updates across services - /// - public record IpFilterChanged : DomainEvent - { - /// - /// IP filter database ID - /// - public int FilterId { get; init; } - - /// - /// IP address or CIDR range - /// - public string IpAddressOrCidr { get; init; } = string.Empty; - - /// - /// Filter type (whitelist/blacklist) - /// - public string FilterType { get; init; } = string.Empty; - - /// - /// Whether the filter is enabled - /// - public bool IsEnabled { get; init; } - - /// - /// Type of change (Created, Updated, Deleted) - /// - public string ChangeType { get; init; } = string.Empty; - - /// - /// Properties that were changed (for selective invalidation) - /// - public string[] ChangedProperties { get; init; } = Array.Empty(); - - /// - /// Filter description for logging - /// - public string Description { get; init; } = string.Empty; - - /// - /// Partition key for ordered processing per filter - /// - public string PartitionKey => FilterId.ToString(); - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Events/MediaCleanupBatchRequested.cs b/ConduitLLM.Core/Events/MediaCleanupBatchRequested.cs deleted file mode 100644 index 6ca81f02b..000000000 --- a/ConduitLLM.Core/Events/MediaCleanupBatchRequested.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace ConduitLLM.Core.Events -{ - /// - /// Event containing a batch of media identified for deletion after retention policy evaluation. - /// Batching improves R2 API efficiency and reduces operation costs. - /// - public record MediaCleanupBatchRequested( - int VirtualKeyGroupId, - List StorageKeys, - string CleanupReason, // "RetentionExpired", "VirtualKeyDeleted", "GroupDeleted", "ManualCleanup" - DateTime ScheduledFor - ) : DomainEvent - { - /// - /// Partition key for ordered processing by virtual key group. - /// - public string PartitionKey => VirtualKeyGroupId.ToString(); - - /// - /// Unique identifier for this batch operation. - /// - public Guid BatchId { get; init; } = Guid.NewGuid(); - - /// - /// Number of items in this batch. - /// - public int BatchSize => StorageKeys?.Count ?? 0; - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Events/MediaCleanupScheduleRequested.cs b/ConduitLLM.Core/Events/MediaCleanupScheduleRequested.cs deleted file mode 100644 index adf9d5ea1..000000000 --- a/ConduitLLM.Core/Events/MediaCleanupScheduleRequested.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace ConduitLLM.Core.Events -{ - /// - /// Event requesting the scheduler to trigger retention checks for all groups. - /// Published by the distributed scheduler service periodically. - /// - public record MediaCleanupScheduleRequested( - DateTime ScheduledAt, - string SchedulerId // Instance ID of the scheduler that triggered this - ) : DomainEvent - { - /// - /// Indicates if this is a dry run (no actual deletions). - /// - public bool IsDryRun { get; init; } = false; - - /// - /// Optional list of specific group IDs to process. - /// Null means process all active groups. - /// - public List? TargetGroupIds { get; init; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Events/MediaRetentionCheckRequested.cs b/ConduitLLM.Core/Events/MediaRetentionCheckRequested.cs deleted file mode 100644 index f121f80fb..000000000 --- a/ConduitLLM.Core/Events/MediaRetentionCheckRequested.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace ConduitLLM.Core.Events -{ - /// - /// Event triggered periodically to evaluate retention policies for a virtual key group. - /// This event initiates the media cleanup evaluation process. - /// - public record MediaRetentionCheckRequested( - int VirtualKeyGroupId, - DateTime RequestedAt, - string Reason // "Scheduled", "BalanceChanged", "PolicyChanged", "Manual" - ) : DomainEvent - { - /// - /// Partition key for ordered processing by virtual key group. - /// Ensures all retention checks for a group are processed sequentially. - /// - public string PartitionKey => VirtualKeyGroupId.ToString(); - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Events/R2BatchDeleteRequested.cs b/ConduitLLM.Core/Events/R2BatchDeleteRequested.cs deleted file mode 100644 index adc68aa0d..000000000 --- a/ConduitLLM.Core/Events/R2BatchDeleteRequested.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace ConduitLLM.Core.Events -{ - /// - /// Event requesting batch deletion of objects from R2 storage. - /// Partitioned by bucket name to prevent concurrent operations on same bucket. - /// - public record R2BatchDeleteRequested( - string BucketName, - List StorageKeys, - int VirtualKeyGroupId, - Guid BatchId - ) : DomainEvent - { - /// - /// Partition key for ordered processing by bucket. - /// Prevents concurrent R2 operations on the same bucket. - /// - public string PartitionKey => BucketName; - - /// - /// Timestamp when this deletion was requested. - /// - public DateTime RequestedAt { get; init; } = DateTime.UtcNow; - - /// - /// Number of objects to delete. - /// - public int ObjectCount => StorageKeys?.Count ?? 0; - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Extensions/LoggingSanitizer.cs b/ConduitLLM.Core/Extensions/LoggingSanitizer.cs deleted file mode 100644 index d447a8ef1..000000000 --- a/ConduitLLM.Core/Extensions/LoggingSanitizer.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Text.RegularExpressions; - -namespace ConduitLLM.Core.Extensions -{ - /// - /// Provides methods to sanitize values for logging to prevent log injection attacks. - /// This class uses patterns that static analysis tools like CodeQL can recognize. - /// - public static class LoggingSanitizer - { - private static readonly Regex CrlfPattern = new(@"[\r\n]", RegexOptions.Compiled); - private static readonly Regex ControlCharPattern = new(@"[\x00-\x1F\x7F]", RegexOptions.Compiled); - private static readonly Regex UnicodeSeparatorPattern = new(@"[\u2028\u2029]", RegexOptions.Compiled); - private const int MaxLength = 1000; - - /// - /// Sanitizes a value for safe logging. This method is designed to be recognized by static analysis tools. - /// - /// The value to sanitize. - /// The sanitized value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static object? S(object? value) - { - if (value == null) return null; - - var str = value.ToString(); - if (str == null) return value; - - // Remove CRLF to prevent log injection - str = CrlfPattern.Replace(str, " "); - - // Remove control characters - str = ControlCharPattern.Replace(str, string.Empty); - - // Remove Unicode line/paragraph separators - str = UnicodeSeparatorPattern.Replace(str, " "); - - // Truncate if too long - if (str.Length > MaxLength) - { - str = str.Substring(0, MaxLength); - } - - return str; - } - - /// - /// Sanitizes a string value for safe logging. - /// - /// The string to sanitize. - /// The sanitized string. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string? S(string? value) - { - if (string.IsNullOrEmpty(value)) return value; - - // Remove CRLF to prevent log injection - value = CrlfPattern.Replace(value, " "); - - // Remove control characters - value = ControlCharPattern.Replace(value, string.Empty); - - // Remove Unicode line/paragraph separators - value = UnicodeSeparatorPattern.Replace(value, " "); - - // Truncate if too long - if (value.Length > MaxLength) - { - value = value.Substring(0, MaxLength); - } - - return value; - } - - /// - /// Sanitizes an integer value (pass-through for type safety). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int S(int value) => value; - - /// - /// Sanitizes a long value (pass-through for type safety). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long S(long value) => value; - - /// - /// Sanitizes a decimal value (pass-through for type safety). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static decimal S(decimal value) => value; - - /// - /// Sanitizes a boolean value (pass-through for type safety). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool S(bool value) => value; - - /// - /// Sanitizes a DateTime value (pass-through for type safety). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static DateTime S(DateTime value) => value; - - /// - /// Sanitizes a Guid value (pass-through for type safety). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Guid S(Guid value) => value; - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Extensions/ServiceCollectionExtensions.cs b/ConduitLLM.Core/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index 012be64e9..000000000 --- a/ConduitLLM.Core/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,261 +0,0 @@ -using ConduitLLM.Configuration.Repositories; -using ConduitLLM.Core.Configuration; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Options; -using ConduitLLM.Core.Routing; -using ConduitLLM.Core.Services; - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Core.Extensions -{ - /// - /// Extension methods for configuring ConduitLLM Core services in an IServiceCollection. - /// - public static class ServiceCollectionExtensions - { - /// - /// Adds the ConduitLLM Context Window Management services to the service collection. - /// - /// The service collection to add services to. - /// The configuration instance. - /// The service collection for chaining. - public static IServiceCollection AddConduitContextManagement(this IServiceCollection services, IConfiguration configuration) - { - // Register configuration options - services.Configure( - configuration.GetSection("ConduitLLM:ContextManagement")); - - // Register model capability service - use database-backed implementation - services.TryAddScoped(); - - // Register token counter - changed to Scoped to match IModelCapabilityService lifetime - services.AddScoped(); - - // Register usage estimation service for streaming responses without usage data - services.AddScoped(); - - // Register context manager - services.AddScoped(); - - return services; - } - - /// - /// Adds the ConduitLLM Audio 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) - { - // 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")); - - return services; - } - - /// - /// Adds the ConduitLLM Batch Cache Invalidation services to the service collection. - /// - /// The service collection to add services to. - /// The configuration instance. - /// The service collection for chaining. - public static IServiceCollection AddBatchCacheInvalidation( - this IServiceCollection services, - IConfiguration configuration) - { - // Register configuration options - services.Configure( - configuration.GetSection("CacheInvalidation")); - - // Register batch service as singleton and hosted service - services.AddSingleton(); - services.AddSingleton(provider => - provider.GetRequiredService()); - services.AddHostedService(provider => - provider.GetRequiredService()); - - return services; - } - - /// - /// Adds the ConduitLLM Discovery Cache services to the service collection. - /// - /// The service collection to add services to. - /// The configuration instance. - /// The service collection for chaining. - public static IServiceCollection AddDiscoveryCache( - this IServiceCollection services, - IConfiguration configuration) - { - // Register configuration options - services.Configure( - configuration.GetSection("Discovery")); - - // Register discovery cache service as singleton for better performance - services.AddSingleton(); - - // Ensure memory cache is registered - services.AddMemoryCache(); - - return services; - } - - /// - /// Adds media storage and lifecycle services to the service collection. - /// Shared configuration used by both Core API and Admin API. - /// - /// The service collection to add services to. - /// The configuration instance. - /// The service collection for chaining. - public static IServiceCollection AddMediaServices(this IServiceCollection services, IConfiguration configuration) - { - // Check multiple sources for storage provider configuration - // Priority: Configuration key > Environment variable from config > Direct environment variable - var configProvider = configuration.GetValue("ConduitLLM:Storage:Provider"); - var configEnvVar = configuration.GetValue("CONDUIT_MEDIA_STORAGE_TYPE"); - var directEnvVar = Environment.GetEnvironmentVariable("CONDUIT_MEDIA_STORAGE_TYPE"); - - var storageProvider = configProvider ?? configEnvVar ?? directEnvVar ?? "InMemory"; - - // Log the selected storage provider for debugging (will be logged when first service is resolved) - Console.WriteLine($"[MediaServices] Storage Provider Selected: {storageProvider}"); - - // Configure media storage based on provider - if (storageProvider.Equals("S3", StringComparison.OrdinalIgnoreCase)) - { - // Configure S3StorageOptions with environment variable mapping - services.Configure(options => - { - // First try to bind from the configuration section - configuration.GetSection(S3StorageOptions.SectionName).Bind(options); - - // Then override with environment variables if they exist - var endpoint = configuration["CONDUIT_S3_ENDPOINT"] ?? Environment.GetEnvironmentVariable("CONDUIT_S3_ENDPOINT"); - if (!string.IsNullOrEmpty(endpoint)) - { - options.ServiceUrl = endpoint; - } - - var accessKey = configuration["CONDUIT_S3_ACCESS_KEY_ID"] - ?? configuration["CONDUIT_S3_ACCESS_KEY"] - ?? Environment.GetEnvironmentVariable("CONDUIT_S3_ACCESS_KEY_ID") - ?? Environment.GetEnvironmentVariable("CONDUIT_S3_ACCESS_KEY"); - if (!string.IsNullOrEmpty(accessKey)) - { - options.AccessKey = accessKey; - } - - var secretKey = configuration["CONDUIT_S3_SECRET_ACCESS_KEY"] - ?? configuration["CONDUIT_S3_SECRET_KEY"] - ?? Environment.GetEnvironmentVariable("CONDUIT_S3_SECRET_ACCESS_KEY") - ?? Environment.GetEnvironmentVariable("CONDUIT_S3_SECRET_KEY"); - if (!string.IsNullOrEmpty(secretKey)) - { - options.SecretKey = secretKey; - } - - var bucketName = configuration["CONDUIT_S3_BUCKET_NAME"] - ?? Environment.GetEnvironmentVariable("CONDUIT_S3_BUCKET_NAME"); - if (!string.IsNullOrEmpty(bucketName)) - { - options.BucketName = bucketName; - } - - var region = configuration["CONDUIT_S3_REGION"] - ?? Environment.GetEnvironmentVariable("CONDUIT_S3_REGION"); - if (!string.IsNullOrEmpty(region)) - { - options.Region = region; - } - - var publicBaseUrl = configuration["CONDUIT_S3_PUBLIC_BASE_URL"] - ?? Environment.GetEnvironmentVariable("CONDUIT_S3_PUBLIC_BASE_URL"); - if (!string.IsNullOrEmpty(publicBaseUrl)) - { - options.PublicBaseUrl = publicBaseUrl; - } - - // Set defaults for S3 compatibility - options.ForcePathStyle = true; - options.AutoCreateBucket = true; - }); - - // Register S3 storage service - services.AddSingleton(); - } - else - { - // Use in-memory storage for development/testing - services.AddSingleton(); - } - - // Configure media management options - services.Configure( - configuration.GetSection("ConduitLLM:MediaManagement")); - - // Register media lifecycle service - services.AddScoped(); - - // Register media lifecycle repository - // MediaLifecycleRepository removed - consolidated into MediaRecordRepository - // Migration: 20250827194408_ConsolidateMediaTables.cs - - 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/ICostCalculationService.cs b/ConduitLLM.Core/Interfaces/ICostCalculationService.cs deleted file mode 100644 index 8750b4bba..000000000 --- a/ConduitLLM.Core/Interfaces/ICostCalculationService.cs +++ /dev/null @@ -1,36 +0,0 @@ -using ConduitLLM.Core.Models; - -namespace ConduitLLM.Core.Interfaces; - -/// -/// Service for calculating the cost of LLM operations based on usage and model information. -/// -public interface ICostCalculationService -{ - /// - /// Calculates the estimated cost of an LLM operation based on usage and model ID. - /// - /// The specific model ID used (e.g., "openai/gpt-4o", "anthropic.claude-3-sonnet-20240229-v1:0"). - /// The usage data returned by the provider. - /// Cancellation token. - /// The calculated cost as a decimal, or 0 if cost cannot be determined. - Task CalculateCostAsync(string modelId, Usage usage, CancellationToken cancellationToken = default); - - /// - /// Calculates a refund for a previous LLM operation. - /// - /// The specific model ID used in the original operation. - /// The original usage data that was charged. - /// The usage data to be refunded. - /// The reason for the refund. - /// Optional original transaction ID for audit trail. - /// Cancellation token. - /// A RefundResult containing the refund details and any validation messages. - Task CalculateRefundAsync( - string modelId, - Usage originalUsage, - Usage refundUsage, - string refundReason, - string? originalTransactionId = null, - CancellationToken cancellationToken = default); -} 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 deleted file mode 100644 index 15b5c8f15..000000000 --- a/ConduitLLM.Core/Interfaces/IModelCapabilityService.cs +++ /dev/null @@ -1,86 +0,0 @@ -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Service for retrieving model capabilities from configuration. - /// Replaces hardcoded model capability detection with database-driven configuration. - /// - public interface IModelCapabilityService - { - /// - /// Determines if a model supports vision/image inputs. - /// - /// The model identifier to check. - /// 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. - /// - /// The model identifier to check. - /// True if the model supports video generation, false otherwise. - Task SupportsVideoGenerationAsync(string model); - - /// - /// Gets the tokenizer type for a model. - /// - /// The model identifier. - /// 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 default model identifier or null if no default is configured. - Task GetDefaultModelAsync(string provider, string capabilityType); - - /// - /// Refreshes the cached model capabilities. - /// Should be called when model configurations are updated. - /// - Task RefreshCacheAsync(); - } -} 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/CacheRegion.cs b/ConduitLLM.Core/Models/CacheRegion.cs deleted file mode 100644 index c0c849294..000000000 --- a/ConduitLLM.Core/Models/CacheRegion.cs +++ /dev/null @@ -1,99 +0,0 @@ -namespace ConduitLLM.Core.Models -{ - /// - /// Defines the cache regions used throughout the Conduit application. - /// Each region represents a logical grouping of cached data with specific characteristics. - /// - public enum CacheRegion - { - /// - /// Virtual keys used for API authentication and authorization. - /// High-security region with immediate invalidation requirements. - /// - VirtualKeys, - - /// - /// Rate limiting data for both IP-based and Virtual Key-based limits. - /// Requires fast access and distributed consistency. - /// - RateLimits, - - /// - /// Provider health status and availability information. - /// Updated frequently based on health checks. - /// - ProviderHealth, - - /// - /// Model capabilities and metadata from providers. - /// Relatively static data with periodic updates. - /// - ModelMetadata, - - /// - /// Authentication tokens for admin and service accounts. - /// Security-critical with strict expiration policies. - /// - AuthTokens, - - /// - /// IP filtering rules for security. - /// Requires immediate propagation of changes. - /// - IpFilters, - - /// - /// Asynchronous task status and results. - /// Short-lived data with automatic cleanup. - /// - AsyncTasks, - - /// - /// Provider response caching for identical requests. - /// Cost optimization with configurable TTL. - /// - ProviderResponses, - - /// - /// Embedding vectors for semantic search and similarity. - /// Large data size with model-specific invalidation. - /// - Embeddings, - - /// - /// Global application settings. - /// Infrequently changed with immediate propagation needs. - /// - GlobalSettings, - - /// - /// Provider credentials for external service authentication. - /// Security-sensitive with encryption requirements. - /// - Providers, - - /// - /// Model cost information for billing calculations. - /// Updated periodically from provider pricing. - /// - ModelCosts, - - /// - /// Audio stream data for real-time processing. - /// Temporary storage with streaming requirements. - /// - AudioStreams, - - /// - /// Alert and monitoring data. - /// Time-series data with retention policies. - /// - Monitoring, - - /// - /// Default region for unspecified cache operations. - /// Should be avoided in favor of specific regions. - /// - Default - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Models/CachedModelCost.cs b/ConduitLLM.Core/Models/CachedModelCost.cs deleted file mode 100644 index f74d30912..000000000 --- a/ConduitLLM.Core/Models/CachedModelCost.cs +++ /dev/null @@ -1,146 +0,0 @@ -using ConduitLLM.Configuration; - -namespace ConduitLLM.Core.Models -{ - /// - /// Represents a cached model cost with pre-parsed pricing configuration for performance. - /// - public class CachedModelCost - { - /// - /// Unique identifier for the model cost entry. - /// - public int Id { get; set; } - - /// - /// User-friendly name for this cost configuration. - /// - public string CostName { get; set; } = string.Empty; - - /// - /// The pricing model type that determines how costs are calculated. - /// - public PricingModel PricingModel { get; set; } = PricingModel.Standard; - - /// - /// Pre-parsed pricing configuration object for performance. - /// Type depends on PricingModel. - /// - public object? ParsedPricingConfiguration { get; set; } - - /// - /// Cost per million input tokens for chat/completion requests. - /// - public decimal InputCostPerMillionTokens { get; set; } = 0; - - /// - /// Cost per million output tokens for chat/completion requests. - /// - public decimal OutputCostPerMillionTokens { get; set; } = 0; - - /// - /// Cost per million tokens for embedding requests, if applicable. - /// - public decimal? EmbeddingCostPerMillionTokens { get; set; } - - /// - /// Cost per image for image generation requests, if applicable. - /// - public decimal? ImageCostPerImage { get; set; } - - /// - /// Cost per second for video generation requests, if applicable. - /// - public decimal? VideoCostPerSecond { get; set; } - - /// - /// Resolution-based cost multipliers for video generation. - /// - public Dictionary? VideoResolutionMultipliers { get; set; } - - /// - /// Quality-based cost multipliers for image generation. - /// - public Dictionary? ImageQualityMultipliers { get; set; } - - /// - /// Resolution-based cost multipliers for image generation. - /// - public Dictionary? ImageResolutionMultipliers { get; set; } - - /// - /// Cost multiplier for batch processing operations, if applicable. - /// - public decimal? BatchProcessingMultiplier { get; set; } - - /// - /// Indicates if this model supports batch processing. - /// - public bool SupportsBatchProcessing { get; set; } - - /// - /// Cost per million cached input tokens for prompt caching, if applicable. - /// - public decimal? CachedInputCostPerMillionTokens { get; set; } - - /// - /// Cost per million tokens for writing to the prompt cache, if applicable. - /// - public decimal? CachedInputWriteCostPerMillionTokens { get; set; } - - /// - /// Cost per search unit for reranking models, if applicable. - /// - public decimal? CostPerSearchUnit { get; set; } - - /// - /// Cost per inference step for image generation models, if applicable. - /// - public decimal? CostPerInferenceStep { get; set; } - - /// - /// Default number of inference steps for this model. - /// - 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. - /// - public string ModelType { get; set; } = "chat"; - - /// - /// Indicates whether this cost configuration is active. - /// - public bool IsActive { get; set; } = true; - - /// - /// Priority value for this model cost entry. - /// - public int Priority { get; set; } = 0; - - /// - /// Optional description for this model cost entry. - /// - public string? Description { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Models/ChatCompletionRequest.cs b/ConduitLLM.Core/Models/ChatCompletionRequest.cs deleted file mode 100644 index f4cdc5dbc..000000000 --- a/ConduitLLM.Core/Models/ChatCompletionRequest.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace ConduitLLM.Core.Models; - -/// -/// Represents a request to create a chat completion. -/// -public class ChatCompletionRequest -{ - /// - /// The ID of the model to use for this completion. - /// - [JsonPropertyName("model")] - public required string Model { get; set; } - - /// - /// A list of messages comprising the conversation so far. - /// - [JsonPropertyName("messages")] - public required List Messages { get; set; } - - /// - /// What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, - /// while lower values like 0.2 will make it more focused and deterministic. - /// - [JsonPropertyName("temperature")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public double? Temperature { get; set; } - - /// - /// The maximum number of tokens to generate in the chat completion. - /// - [JsonPropertyName("max_tokens")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public int? MaxTokens { get; set; } - - /// - /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of - /// the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. - /// - [JsonPropertyName("top_p")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public double? TopP { get; set; } - - /// - /// Limits the number of tokens to consider for each step of generation. - /// Only the top K most likely tokens are considered for sampling. - /// Typical values range from 1 to 100. Lower values make output more focused. - /// - /// - /// Top-k sampling is a technique that restricts the model to only consider - /// the K most likely next tokens at each step. This can help prevent the model - /// from selecting very unlikely tokens and can make the output more coherent. - /// Not all providers support this parameter. - /// - [JsonPropertyName("top_k")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public int? TopK { get; set; } - - /// - /// How many chat completion choices to generate for each input message. - /// - [JsonPropertyName("n")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public int? N { get; set; } - - /// - /// If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only server-sent events - /// as they become available, with the stream terminated by a data: [DONE] message. - /// - [JsonPropertyName("stream")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public bool? Stream { get; set; } - - /// - /// Up to 4 sequences where the API will stop generating further tokens. - /// - [JsonPropertyName("stop")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public List? Stop { get; set; } - - /// - /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. - /// - [JsonPropertyName("user")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? User { get; set; } - - /// - /// A list of tools the model may call. Currently, only functions are supported as tools. - /// - [JsonPropertyName("tools")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public List? Tools { get; set; } - - /// - /// Controls which (if any) tool is called by the model. "none" means the model will not call a tool and instead generates a message. - /// "auto" means the model can choose either to call a tool or not. Specifying a particular function forces the model to call that function. - /// - [JsonPropertyName("tool_choice")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public ToolChoice? ToolChoice { get; set; } - - /// - /// Specifies the format that the model must output. - /// - /// - /// - /// This property allows you to control the format of the model's response. For example, - /// you can request the model to respond with valid JSON by setting ResponseFormat.Type to "json_object". - /// - /// - /// See for details on available format options. - /// - /// - /// - /// - /// // Request JSON response - /// var request = new ChatCompletionRequest - /// { - /// // ... other properties - /// ResponseFormat = ResponseFormat.Json() - /// }; - /// - /// - [JsonPropertyName("response_format")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public ResponseFormat? ResponseFormat { get; set; } - - /// - /// A random number seed for deterministic outputs. - /// - [JsonPropertyName("seed")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public int? Seed { get; set; } - - /// - /// Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear - /// in the text so far, increasing the model's likelihood to talk about new topics. - /// - [JsonPropertyName("presence_penalty")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public double? PresencePenalty { get; set; } - - /// - /// Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing - /// frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. - /// - [JsonPropertyName("frequency_penalty")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public double? FrequencyPenalty { get; set; } - - /// - /// Modify the likelihood of specified tokens appearing in the completion. - /// - [JsonPropertyName("logit_bias")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public Dictionary? LogitBias { get; set; } - - /// - /// The system fingerprint, a unique identifier for the configuration used by OpenAI systems for this request. - /// - [JsonPropertyName("system_fingerprint")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? SystemFingerprint { get; set; } - - /// - /// Additional model-specific parameters that are passed through to the provider API. - /// These parameters are populated based on the model's ApiParameters configuration. - /// Examples include reasoning_effort, min_p, language, timestamp_granularities, etc. - /// - /// - /// This property captures any JSON properties not explicitly mapped to other properties. - /// The router validates these against the model's supported parameters before forwarding. - /// - [JsonExtensionData] - public Dictionary? ExtensionData { get; set; } -} diff --git a/ConduitLLM.Core/Models/DeltaContent.cs b/ConduitLLM.Core/Models/DeltaContent.cs deleted file mode 100644 index 200784739..000000000 --- a/ConduitLLM.Core/Models/DeltaContent.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Text.Json.Serialization; - -namespace ConduitLLM.Core.Models; - -/// -/// Represents the delta content within a streaming choice. -/// -public class DeltaContent -{ - /// - /// The role of the author of this message. - /// - [JsonPropertyName("role")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Role { get; set; } - - /// - /// The contents of the message. - /// - [JsonPropertyName("content")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Content { get; set; } - - /// - /// The tool calls made by the assistant in this delta chunk. - /// - [JsonPropertyName("tool_calls")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public List? ToolCalls { 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 deleted file mode 100644 index 678202925..000000000 --- a/ConduitLLM.Core/Models/RefundResult.cs +++ /dev/null @@ -1,180 +0,0 @@ -namespace ConduitLLM.Core.Models -{ - /// - /// Represents the result of a refund calculation operation. - /// - public class RefundResult - { - /// - /// Gets or sets the model ID for which the refund was calculated. - /// - public string ModelId { get; set; } = string.Empty; - - /// - /// Gets or sets the original usage data that was charged. - /// - public Usage OriginalUsage { get; set; } = new Usage { PromptTokens = 0, CompletionTokens = 0, TotalTokens = 0 }; - - /// - /// Gets or sets the usage data being refunded. - /// - public Usage RefundUsage { get; set; } = new Usage { PromptTokens = 0, CompletionTokens = 0, TotalTokens = 0 }; - - /// - /// Gets or sets the total refund amount (always positive). - /// - public decimal RefundAmount { 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 breakdown of the refund by component. - /// - public RefundBreakdown? Breakdown { get; set; } - } - - /// - /// Represents the breakdown of a refund by component. - /// - public class RefundBreakdown - { - /// - /// Gets or sets the refund amount for input tokens. - /// - public decimal InputTokenRefund { get; set; } - - /// - /// Gets or sets the refund amount for output tokens. - /// - public decimal OutputTokenRefund { get; set; } - - /// - /// Gets or sets the refund amount for image generation. - /// - public decimal ImageRefund { get; set; } - - /// - /// Gets or sets the refund amount for video generation. - /// - public decimal VideoRefund { get; set; } - - /// - /// Gets or sets the refund amount for embeddings. - /// - public decimal EmbeddingRefund { get; set; } - - /// - /// Gets or sets the refund amount for search units (reranking operations). - /// - public decimal SearchUnitRefund { get; set; } - - /// - /// Gets or sets the refund amount for inference steps (image generation). - /// - 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/README.md b/ConduitLLM.Core/README.md deleted file mode 100644 index aa20d0562..000000000 --- a/ConduitLLM.Core/README.md +++ /dev/null @@ -1,411 +0,0 @@ -# ConduitLLM.Core - -## Overview - -**ConduitLLM.Core** is the foundational library of the ConduitLLM system, providing a unified abstraction layer for interacting with multiple Large Language Model (LLM) providers. It offers sophisticated routing, context management, and orchestration capabilities for enterprise-scale LLM applications. - -## Architecture - -The ConduitLLM system follows a modular architecture: - -- **ConduitLLM.Core**: Central orchestration, interfaces, models, routing, and provider abstraction -- **ConduitLLM.Providers**: Provider-specific implementations for OpenAI, Anthropic, Google, and other LLM services -- **ConduitLLM.Configuration**: Centralized configuration management and validation -- **ConduitLLM.Http**: RESTful API layer for external integrations -- **ConduitLLM.Security**: Authentication, authorization, and security utilities -- **ConduitLLM.Admin**: Administrative interface and management tools - -## Core Capabilities - -### Multi-Provider Support -- **Unified API**: Single interface for OpenAI, Anthropic, Google, Azure, and custom providers -- **Provider-specific optimizations**: Leverage unique features of each LLM service -- **Fallback mechanisms**: Automatic failover between providers for reliability - -### Advanced Routing -- **Intelligent load balancing**: Round-robin, least-used, random, and custom strategies -- **Model aliasing**: Abstract model names from provider-specific identifiers -- **Priority routing**: Route requests based on cost, latency, or quality requirements - -### Context Management -- **Token optimization**: Automatic context window management and message trimming -- **Conversation history**: Intelligent conversation state management -- **Cost optimization**: Dynamic context sizing based on model pricing - -### Enterprise Features -- **Health monitoring**: Built-in health checks and provider status monitoring -- **Metrics collection**: Prometheus-compatible metrics for observability -- **Circuit breakers**: Resilience patterns with Polly integration -- **Caching**: Redis-based caching for embeddings and responses -- **Message queuing**: MassTransit integration for async processing - -## Key Components - -### Core Classes -- **Conduit**: Primary orchestration class for all LLM operations -- **ILLMClientFactory**: Factory pattern for provider client instantiation -- **ILLMRouter**: Routing strategy implementations -- **IContextManager**: Context window and conversation management - -### Data Models -- **ChatCompletionRequest/Response**: Standardized chat interfaces -- **EmbeddingRequest/Response**: Text embedding operations -- **ImageGenerationRequest/Response**: DALL-E, Stable Diffusion, and other image models -- **Streaming support**: Real-time response streaming for all operations - -### Infrastructure -- **HealthChecks**: Provider health monitoring and status reporting -- **Caching**: Multi-level caching for performance optimization -- **Metrics**: Comprehensive telemetry and monitoring -- **Validation**: Request/response validation and sanitization - -## Key Features - -### Provider Support -- **OpenAI**: GPT-4, GPT-3.5-turbo, DALL-E, Whisper, and embeddings -- **Anthropic**: Claude 3.x series (Haiku, Sonnet, Opus) -- **Google**: Gemini Pro and Vision models -- **Azure OpenAI**: Enterprise-grade OpenAI models -- **Custom Providers**: Extensible interface for proprietary models - -### Routing Strategies -- **Simple**: Direct routing to specified models -- **RoundRobin**: Distribute load evenly across available models -- **LeastUsed**: Route to the least recently used provider -- **Random**: Random selection with optional weighting -- **Passthrough**: Bypass routing for direct model access - -### Enterprise Integrations -- **AWS S3**: Model artifact storage and retrieval -- **Redis**: Distributed caching and session management -- **Prometheus**: Metrics collection and monitoring -- **MassTransit**: Message queuing and async processing -- **HealthChecks**: Comprehensive health monitoring system - -## Core Components - -### Primary Classes -- **Conduit**: Main orchestrator for chat completions, streaming, embeddings, and image generation -- **ILLMClient**: Unified interface for all LLM provider interactions -- **ILLMClientFactory**: Factory for creating provider-specific clients -- **ILLMRouter**: Intelligent routing and load balancing -- **IContextManager**: Context window and conversation management - -### Supporting Infrastructure -- **Interfaces/**: Contracts for extensibility and dependency injection -- **Models/**: Comprehensive request/response DTOs -- **Exceptions/**: Rich error handling with specific exception types -- **Configuration/**: Environment-based configuration management -- **HealthChecks/**: Provider health monitoring and diagnostics -- **Caching/**: Multi-tier caching strategies -- **Validation/**: Input validation and sanitization -- **Utilities/**: Common helpers and extensions - -## Configuration & Environment - -### Database Configuration -The `DbConnectionHelper` provides zero-configuration database connectivity: - -**PostgreSQL:** -```bash -DATABASE_URL=postgresql://user:password@host:5432/conduit_db -``` - -**SQLite:** -```bash -CONDUIT_SQLITE_PATH=/data/conduit_config.db -``` - -**Auto-detection**: Automatically detects provider type and configures Entity Framework Core - -### Environment Variables -```bash -# Core Configuration -CONDUIT_ENVIRONMENT=Development|Staging|Production -CONDUIT_LOG_LEVEL=Debug|Information|Warning|Error - -# Provider Configuration -OPENAI_API_KEY=sk-... -ANTHROPIC_API_KEY=sk-ant-... -GOOGLE_API_KEY=... - -# Optional Features -REDIS_CONNECTION_STRING=localhost:6379 -PROMETHEUS_ENABLED=true -HEALTH_CHECK_INTERVAL=30 -``` - -## Usage Patterns - -### Basic Setup (Dependency Injection) -```csharp -// Program.cs or Startup.cs -services.AddConduitLLM(configuration); - -// Usage -public class MyService -{ - private readonly IConduit _conduit; - - public MyService(IConduit conduit) - { - _conduit = conduit; - } - - public async Task GenerateResponse(string prompt) - { - var request = new ChatCompletionRequest - { - Model = "router:roundrobin:gpt-4", - Messages = new[] - { - new ChatMessage { Role = "user", Content = prompt } - } - }; - - var response = await _conduit.CreateChatCompletionAsync(request); - return response.Choices.First().Message.Content; - } -} -``` - -### Advanced Configuration -```csharp -// Custom routing strategy -services.Configure(options => -{ - options.DefaultStrategy = "leastused"; - options.EnableContextManagement = true; - options.MaxTokens = 4000; -}); - -// Provider-specific settings -services.Configure(configuration.GetSection("OpenAI")); -services.Configure(configuration.GetSection("Anthropic")); -``` - -### Direct Usage (Advanced) -```csharp -var factory = new LLMClientFactory(configuration); -var router = new RoundRobinRouter(factory); -var conduit = new Conduit(factory, logger, router); - -// Streaming example -await foreach (var chunk in conduit.StreamChatCompletionAsync(request)) -{ - Console.Write(chunk.Content); -} -``` - -## Routing & Model Management - -### Routing Syntax -``` -router:[strategy]:[model] -router:roundrobin:gpt-4 -router:leastused:claude-3-sonnet -router:random -``` - -### Model Aliases -Configure provider-specific model mappings in `appsettings.json`: - -```json -{ - "ConduitLLM": { - "ModelMappings": { - "gpt-4": "openai:gpt-4-turbo-preview", - "claude-3": "anthropic:claude-3-sonnet-20240229", - "gemini": "google:gemini-pro" - } - } -} -``` - -### Advanced Routing -- **Weighted routing**: Assign weights to providers for cost optimization -- **Fallback chains**: Define fallback sequences for reliability -- **Region-aware routing**: Route based on geographic latency -- **Quota management**: Automatic provider switching based on usage limits - -## Configuration Management - -### Application Configuration -```json -{ - "ConduitLLM": { - "Providers": { - "OpenAI": { - "ApiKey": "${OPENAI_API_KEY}", - "BaseUrl": "https://api.openai.com/v1", - "Models": ["gpt-4", "gpt-3.5-turbo", "text-embedding-ada-002"] - }, - "Anthropic": { - "ApiKey": "${ANTHROPIC_API_KEY}", - "BaseUrl": "https://api.anthropic.com", - "Models": ["claude-3-sonnet", "claude-3-opus"] - } - }, - "Routing": { - "DefaultStrategy": "roundrobin", - "EnableFallback": true, - "MaxRetries": 3 - }, - "ContextManagement": { - "Enabled": true, - "MaxTokens": 4000, - "TrimStrategy": "oldest" - } - } -} -``` - -### Environment-based Configuration -All configuration supports environment variable substitution and validation. - -## Dependencies - -### Core Framework -- **.NET 9.0** - Latest LTS framework with performance optimizations -- **C# 12.0** - Modern language features and performance improvements - -### Microsoft Extensions -- **Microsoft.Extensions.Options** (9.0.7) - Configuration management -- **Microsoft.Extensions.Logging.Abstractions** (9.0.7) - Structured logging -- **Microsoft.Extensions.Diagnostics.HealthChecks** (9.0.7) - Health monitoring - -### Enterprise Libraries -- **AWSSDK.S3** (4.0.5) - AWS S3 integration for model artifacts -- **Polly** (8.6.2) - Resilience patterns and circuit breakers -- **prometheus-net** (8.2.1) - Metrics collection and monitoring -- **MassTransit** (8.5.1) - Message queuing and async processing -- **MassTransit.Redis** (8.5.1) - Redis transport for MassTransit -- **TiktokenSharp** (1.1.7) - Token counting and optimization - -### Project Dependencies -- **ConduitLLM.Configuration** - Shared configuration models and validation -- **ConduitLLM.Providers** - Provider-specific implementations (indirect) - -## Extensibility Guide - -### Adding New LLM Providers - -1. **Create Provider Implementation** - ```csharp - public class CustomLLMClient : ILLMClient - { - public async Task CreateChatCompletionAsync( - ChatCompletionRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - // Implementation for your LLM provider - } - - // Implement other required methods... - } - ``` - -2. **Register in Factory** - ```csharp - public class CustomClientFactory : ILLMClientFactory - { - public ILLMClient GetClient(string modelAlias) - { - if (modelAlias.StartsWith("custom:")) - return new CustomLLMClient(); - // ... other providers - } - } - ``` - -3. **Add Configuration** - ```json - { - "CustomProvider": { - "ApiKey": "${CUSTOM_API_KEY}", - "BaseUrl": "https://api.custom-llm.com", - "Models": ["custom-model-1", "custom-model-2"] - } - } - ``` - -### Custom Routing Strategies - -1. **Implement ILLMRouter** - ```csharp - public class PriorityRouter : ILLMRouter - { - public Task GetClientAsync( - string model, - CancellationToken cancellationToken = default) - { - // Custom routing logic based on priority, cost, or other factors - } - } - ``` - -2. **Register Custom Router** - ```csharp - services.AddSingleton(); - ``` - -## Development & Testing - -### Development Setup -```bash -# Clone repository -git clone [repository-url] -cd Conduit - -# Restore dependencies -dotnet restore - -# Build solution -dotnet build - -# Run tests -dotnet test - -# Run specific test project -dotnet test ConduitLLM.Tests -dotnet test ConduitLLM.Http.Tests -dotnet test ConduitLLM.Admin.Tests -``` - -### Testing Strategy -- **Unit Tests**: Core logic validation in `ConduitLLM.Tests` -- **Integration Tests**: API testing in `ConduitLLM.Http.Tests` -- **Admin Tests**: Administrative interface testing in `ConduitLLM.Admin.Tests` -- **Load Testing**: Performance and scalability validation -- **Provider Tests**: Individual provider integration validation - -### Contributing -1. Fork the repository -2. Create feature branch (`git checkout -b feature/amazing-feature`) -3. Commit changes (`git commit -m 'Add amazing feature'`) -4. Push to branch (`git push origin feature/amazing-feature`) -5. Open Pull Request - -### Performance Benchmarks -- **Throughput**: >1000 requests/second (depends on provider limits) -- **Latency**: <100ms overhead (excluding provider latency) -- **Memory**: <50MB baseline memory usage -- **Scalability**: Horizontal scaling with Redis clustering - -## License & Support - -### License -See the root of the repository for license information. - -### Support -- **Documentation**: [Full documentation](https://docs.conduit-llm.com) -- **Issues**: [GitHub Issues](https://github.com/[org]/conduit-llm/issues) -- **Discussions**: [GitHub Discussions](https://github.com/[org]/conduit-llm/discussions) -- **Security**: Report security issues to security@conduit-llm.com - -### Roadmap -- **Q2 2025**: Enhanced multi-modal support (video, audio) -- **Q3 2025**: Advanced caching strategies with semantic search -- **Q4 2025**: Federated learning integration -- **Q1 2026**: Edge deployment optimizations 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/BatchOperations/BatchSpendUpdateOperation.cs b/ConduitLLM.Core/Services/BatchOperations/BatchSpendUpdateOperation.cs deleted file mode 100644 index d184f4f8d..000000000 --- a/ConduitLLM.Core/Services/BatchOperations/BatchSpendUpdateOperation.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Microsoft.Extensions.Logging; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using IVirtualKeyService = ConduitLLM.Core.Interfaces.IVirtualKeyService; -namespace ConduitLLM.Core.Services.BatchOperations -{ - /// - /// Batch operation for updating spend amounts across multiple virtual keys - /// - public class BatchSpendUpdateOperation : IBatchSpendUpdateOperation - { - private readonly ILogger _logger; - private readonly IBatchOperationService _batchOperationService; - private readonly IVirtualKeyService _virtualKeyService; - private readonly ISpendNotificationService _spendNotificationService; - - public BatchSpendUpdateOperation( - ILogger logger, - IBatchOperationService batchOperationService, - IVirtualKeyService virtualKeyService, - ISpendNotificationService spendNotificationService) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _batchOperationService = batchOperationService ?? throw new ArgumentNullException(nameof(batchOperationService)); - _virtualKeyService = virtualKeyService ?? throw new ArgumentNullException(nameof(virtualKeyService)); - _spendNotificationService = spendNotificationService ?? throw new ArgumentNullException(nameof(spendNotificationService)); - } - - /// - /// Execute batch spend update operation - /// - public async Task ExecuteAsync( - List spendUpdates, - int virtualKeyId, - CancellationToken cancellationToken = default) - { - var options = new BatchOperationOptions - { - VirtualKeyId = virtualKeyId, - MaxDegreeOfParallelism = 10, // Limit parallelism for database operations - ContinueOnError = true, - EnableCheckpointing = true, - CheckpointInterval = 50, - Metadata = new Dictionary - { - ["updateType"] = "spend_batch_update", - ["source"] = "batch_operation" - } - }; - - return await _batchOperationService.StartBatchOperationAsync( - "spend_update", - spendUpdates, - ProcessSpendUpdateAsync, - options, - cancellationToken); - } - - private async Task ProcessSpendUpdateAsync( - SpendUpdateItem item, - CancellationToken cancellationToken) - { - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - - try - { - // Validate virtual key exists - var virtualKey = await _virtualKeyService.GetVirtualKeyInfoForValidationAsync(item.VirtualKeyId, cancellationToken); - if (virtualKey == null) - { - return new BatchItemResult - { - Success = false, - ItemIdentifier = $"VKey-{item.VirtualKeyId}", - Error = "Virtual key not found", - Duration = stopwatch.Elapsed - }; - } - - // Apply spend update - await _virtualKeyService.UpdateSpendAsync(item.VirtualKeyId, item.Amount); - - // Send real-time notification - await _spendNotificationService.NotifySpendUpdatedAsync( - item.VirtualKeyId, - item.Amount, - item.Model, - item.Provider); - - return new BatchItemResult - { - Success = true, - ItemIdentifier = $"VKey-{item.VirtualKeyId}", - Duration = stopwatch.Elapsed, - Data = new - { - VirtualKeyId = item.VirtualKeyId, - Amount = item.Amount - } - }; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Failed to process spend update for virtual key {VirtualKeyId}", - item.VirtualKeyId); - - return new BatchItemResult - { - Success = false, - ItemIdentifier = $"VKey-{item.VirtualKeyId}", - Error = ex.Message, - Duration = stopwatch.Elapsed - }; - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/ConfigurationModelCapabilityService.cs b/ConduitLLM.Core/Services/ConfigurationModelCapabilityService.cs deleted file mode 100644 index a3f23cf58..000000000 --- a/ConduitLLM.Core/Services/ConfigurationModelCapabilityService.cs +++ /dev/null @@ -1,196 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Configuration; - -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace ConduitLLM.Core.Services -{ - /// - /// Configuration-based implementation of IModelCapabilityService. - /// Loads model capabilities from configuration files instead of hardcoded values. - /// - public class ConfigurationModelCapabilityService : IModelCapabilityService - { - private readonly ILogger _logger; - private readonly IMemoryCache _cache; - private readonly IOptionsMonitor _modelConfig; - private readonly SemaphoreSlim _cacheLock = new(1, 1); - - private const string CacheKeyPrefix = "ModelCapability:"; - private const int CacheExpirationMinutes = 60; - - public ConfigurationModelCapabilityService( - ILogger logger, - IMemoryCache cache, - IOptionsMonitor modelConfig) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _cache = cache ?? throw new ArgumentNullException(nameof(cache)); - _modelConfig = modelConfig ?? throw new ArgumentNullException(nameof(modelConfig)); - - // Listen for configuration changes - _modelConfig.OnChange(_ => - { - _logger.LogInformation("Model configuration changed, clearing cache"); - Task.Run(async () => await RefreshCacheAsync()); - }); - } - - public async Task SupportsVisionAsync(string model) - { - var capability = await GetModelCapabilityAsync(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) - { - var capability = await GetModelCapabilityAsync(model); - return capability?.SupportsVideoGeneration ?? false; - } - - public async Task GetTokenizerTypeAsync(string model) - { - var capability = await GetModelCapabilityAsync(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) - { - var cacheKey = $"{CacheKeyPrefix}Default:{provider}:{capabilityType}"; - - return await _cache.GetOrCreateAsync(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(CacheExpirationMinutes); - - var config = _modelConfig.CurrentValue; - var providerDefaults = config.ProviderDefaults.FirstOrDefault(p => - p.Provider.Equals(provider, StringComparison.OrdinalIgnoreCase)); - - if (providerDefaults?.DefaultModels.TryGetValue(capabilityType, out var defaultModel) == true) - { - return Task.FromResult(defaultModel); - } - - // Fallback: find first enabled model with the capability - var models = config.Models.Where(m => - m.Provider.Equals(provider, StringComparison.OrdinalIgnoreCase) && - m.Enabled); - - var result = capabilityType.ToLowerInvariant() switch - { - "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 - }; - - return Task.FromResult(result); - }); - } - - public async Task RefreshCacheAsync() - { - await _cacheLock.WaitAsync(); - try - { - _logger.LogInformation("Refreshing model capability cache"); - - // Clear all cache entries with our prefix - var cacheKeys = new List(); - - // In production, you'd track cache keys or use a distributed cache with pattern support - // For now, we'll clear specific known patterns - var config = _modelConfig.CurrentValue; - foreach (var model in config.Models) - { - _cache.Remove($"{CacheKeyPrefix}Model:{model.ModelId}"); - } - - foreach (var provider in config.ProviderDefaults) - { - foreach (var capabilityType in provider.DefaultModels.Keys) - { - _cache.Remove($"{CacheKeyPrefix}Default:{provider.Provider}:{capabilityType}"); - } - } - - _logger.LogInformation("Model capability cache refreshed"); - } - finally - { - _cacheLock.Release(); - } - } - - private async Task GetModelCapabilityAsync(string model) - { - if (string.IsNullOrWhiteSpace(model)) - { - _logger.LogWarning("Model identifier is null or empty"); - return null; - } - - var cacheKey = $"{CacheKeyPrefix}Model:{model}"; - - return await _cache.GetOrCreateAsync(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(CacheExpirationMinutes); - - var config = _modelConfig.CurrentValue; - var modelConfig = config.Models.FirstOrDefault(m => - m.ModelId.Equals(model, StringComparison.OrdinalIgnoreCase) && - m.Enabled); - - if (modelConfig == null) - { - _logger.LogDebug("Model {Model} not found in configuration", model); - return Task.FromResult(null); - } - - _logger.LogDebug("Loaded capabilities for model {Model} from configuration", model); - return Task.FromResult(modelConfig.Capabilities); - }); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/CostCalculationService.PricingModels.cs b/ConduitLLM.Core/Services/CostCalculationService.PricingModels.cs deleted file mode 100644 index 0dcc860bf..000000000 --- a/ConduitLLM.Core/Services/CostCalculationService.PricingModels.cs +++ /dev/null @@ -1,269 +0,0 @@ -using System.Text.Json; - -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Models.Pricing; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services; - -/// -/// Service implementation for various pricing model cost calculations -/// -public partial class CostCalculationService -{ - private Task CalculatePerVideoCostAsync(string modelId, ModelCost modelCost, Usage usage) - { - if (!usage.VideoDurationSeconds.HasValue || string.IsNullOrEmpty(usage.VideoResolution)) - { - _logger.LogDebug("No video usage data for per-video pricing model {ModelId}", modelId); - return Task.FromResult(0m); - } - - // Parse configuration from JSON - PerVideoPricingConfig? config = null; - if (!string.IsNullOrEmpty(modelCost.PricingConfiguration)) - { - try - { - config = JsonSerializer.Deserialize(modelCost.PricingConfiguration); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to parse per-video pricing configuration for model {ModelId}", modelId); - throw new InvalidOperationException($"Invalid per-video pricing configuration for model {modelId}"); - } - } - - if (config == null || config.Rates == null || config.Rates.Count() == 0) - { - _logger.LogError("No per-video pricing rates configured for model {ModelId}", modelId); - throw new InvalidOperationException($"No per-video pricing rates configured for model {modelId}"); - } - - // Build lookup key (e.g., "720p_6" for 720p resolution, 6 seconds) - var duration = (int)Math.Round(usage.VideoDurationSeconds.Value); - var lookupKey = $"{usage.VideoResolution}_{duration}"; - - if (!config.Rates.TryGetValue(lookupKey, out var flatRate)) - { - _logger.LogError("No pricing found for video {Resolution} {Duration}s for model {ModelId}", - usage.VideoResolution, duration, modelId); - throw new InvalidOperationException($"No pricing available for {usage.VideoResolution} {duration}s video on model {modelId}"); - } - - _logger.LogDebug("Per-video cost for model {ModelId}: {Resolution} {Duration}s = ${Cost}", - modelId, usage.VideoResolution, duration, flatRate); - - return Task.FromResult(flatRate); - } - - private Task CalculatePerSecondVideoCostAsync(string modelId, ModelCost modelCost, Usage usage) - { - if (!usage.VideoDurationSeconds.HasValue) - { - _logger.LogDebug("No video duration for per-second video pricing model {ModelId}", modelId); - return Task.FromResult(0m); - } - - // Parse configuration from JSON - PerSecondVideoPricingConfig? config = null; - if (!string.IsNullOrEmpty(modelCost.PricingConfiguration)) - { - try - { - config = JsonSerializer.Deserialize(modelCost.PricingConfiguration); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to parse per-second video pricing configuration for model {ModelId}", modelId); - throw new InvalidOperationException($"Invalid per-second video pricing configuration for model {modelId}"); - } - } - - if (config == null) - { - _logger.LogError("No per-second video pricing configuration for model {ModelId}", modelId); - throw new InvalidOperationException($"No per-second video pricing configuration for model {modelId}"); - } - - var baseCost = (decimal)usage.VideoDurationSeconds.Value * config.BaseRate; - - // Apply resolution multiplier if available - if (!string.IsNullOrEmpty(usage.VideoResolution) && - config.ResolutionMultipliers != null && - config.ResolutionMultipliers.TryGetValue(usage.VideoResolution, out var multiplier)) - { - baseCost *= multiplier; - _logger.LogDebug("Applied video resolution multiplier {Multiplier} for {Resolution}", multiplier, usage.VideoResolution); - } - - _logger.LogDebug("Per-second video cost for model {ModelId}: {Duration}s × ${BaseRate} = ${Cost}", - modelId, usage.VideoDurationSeconds.Value, config.BaseRate, baseCost); - - return Task.FromResult(baseCost); - } - - private Task CalculateInferenceStepsCostAsync(string modelId, ModelCost modelCost, Usage usage) - { - // Parse configuration or use pre-parsed - InferenceStepsPricingConfig? config = null; - if (!string.IsNullOrEmpty(modelCost.PricingConfiguration)) - { - try - { - config = JsonSerializer.Deserialize(modelCost.PricingConfiguration); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to parse inference steps pricing configuration for model {ModelId}", modelId); - throw new InvalidOperationException($"Invalid inference steps pricing configuration for model {modelId}"); - } - } - - if (config == null) - { - _logger.LogError("No inference steps pricing configuration for model {ModelId}", modelId); - throw new InvalidOperationException($"No inference steps pricing configuration for model {modelId}"); - } - - // Use provided steps or default - var steps = usage.InferenceSteps ?? config.DefaultSteps; - if (steps <= 0) - { - _logger.LogDebug("No inference steps for model {ModelId}", modelId); - return Task.FromResult(0m); - } - - var cost = steps * config.CostPerStep; - - _logger.LogDebug("Inference steps cost for model {ModelId}: {Steps} steps × ${CostPerStep} = ${Cost}", - modelId, steps, config.CostPerStep, cost); - - return Task.FromResult(cost); - } - - private Task CalculateTieredTokensCostAsync(string modelId, ModelCost modelCost, Usage usage) - { - // Parse configuration or use pre-parsed - TieredTokensPricingConfig? config = null; - if (!string.IsNullOrEmpty(modelCost.PricingConfiguration)) - { - try - { - config = JsonSerializer.Deserialize(modelCost.PricingConfiguration); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to parse tiered tokens pricing configuration for model {ModelId}", modelId); - throw new InvalidOperationException($"Invalid tiered tokens pricing configuration for model {modelId}"); - } - } - - if (config == null || config.Tiers == null || config.Tiers.Count() == 0) - { - _logger.LogError("No tiered tokens pricing configuration for model {ModelId}", modelId); - throw new InvalidOperationException($"No tiered tokens pricing configuration for model {modelId}"); - } - - var inputTokens = usage.PromptTokens ?? 0; - var outputTokens = usage.CompletionTokens ?? 0; - - // Find the appropriate tier based on total context length - var totalTokens = inputTokens + outputTokens; - TokenPricingTier? tier = null; - - foreach (var t in config.Tiers.OrderBy(t => t.MaxContext ?? int.MaxValue)) - { - if (!t.MaxContext.HasValue || totalTokens <= t.MaxContext.Value) - { - tier = t; - break; - } - } - - if (tier == null) - { - tier = config.Tiers.Last(); // Use highest tier if none match - } - - var inputCost = (inputTokens * tier.InputCost) / 1_000_000m; - var outputCost = (outputTokens * tier.OutputCost) / 1_000_000m; - - _logger.LogDebug("Tiered tokens cost for model {ModelId}: Context {TotalTokens}, Tier ≤{MaxContext}, " + - "Input: {InputTokens} × ${InputRate} + Output: {OutputTokens} × ${OutputRate} = ${TotalCost}", - modelId, totalTokens, tier.MaxContext, inputTokens, tier.InputCost, outputTokens, tier.OutputCost, inputCost + outputCost); - - return Task.FromResult(inputCost + outputCost); - } - - private Task CalculatePerImageCostAsync(string modelId, ModelCost modelCost, Usage usage) - { - if (!usage.ImageCount.HasValue || usage.ImageCount.Value <= 0) - { - _logger.LogDebug("No image count for per-image pricing model {ModelId}", modelId); - return Task.FromResult(0m); - } - - // Parse configuration or use pre-parsed - PerImagePricingConfig? config = null; - if (!string.IsNullOrEmpty(modelCost.PricingConfiguration)) - { - try - { - config = JsonSerializer.Deserialize(modelCost.PricingConfiguration); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to parse per-image pricing configuration for model {ModelId}", modelId); - throw new InvalidOperationException($"Invalid per-image pricing configuration for model {modelId}"); - } - } - - if (config == null) - { - _logger.LogError("No per-image pricing configuration for model {ModelId}", modelId); - throw new InvalidOperationException($"No per-image pricing configuration for model {modelId}"); - } - - var cost = usage.ImageCount.Value * config.BaseRate; - - // Apply quality multiplier - if (!string.IsNullOrEmpty(usage.ImageQuality) && - config.QualityMultipliers != null && - config.QualityMultipliers.TryGetValue(usage.ImageQuality.ToLowerInvariant(), out var qualityMultiplier)) - { - cost *= qualityMultiplier; - _logger.LogDebug("Applied image quality multiplier {Multiplier} for {Quality}", qualityMultiplier, usage.ImageQuality); - } - - // Apply resolution multiplier - if (!string.IsNullOrEmpty(usage.ImageResolution) && - config.ResolutionMultipliers != null && - config.ResolutionMultipliers.TryGetValue(usage.ImageResolution, out var resolutionMultiplier)) - { - cost *= resolutionMultiplier; - _logger.LogDebug("Applied image resolution multiplier {Multiplier} for {Resolution}", resolutionMultiplier, usage.ImageResolution); - } - - _logger.LogDebug("Per-image cost for model {ModelId}: {Count} images × ${BaseRate} = ${Cost}", - modelId, usage.ImageCount.Value, config.BaseRate, cost); - - 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); - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/CostCalculationService.StandardPricing.cs b/ConduitLLM.Core/Services/CostCalculationService.StandardPricing.cs deleted file mode 100644 index 10feaf7d5..000000000 --- a/ConduitLLM.Core/Services/CostCalculationService.StandardPricing.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System.Text.Json; - -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services; - -/// -/// Service implementation for standard pricing model cost calculations -/// -public partial class CostCalculationService -{ - private Task CalculateStandardCostAsync(string modelId, ModelCost modelCost, Usage usage) - { - decimal calculatedCost = 0m; - - // Calculate cost based on token usage - // For embeddings: prioritize embedding cost when available and no completion tokens - if (modelCost.EmbeddingCostPerMillionTokens.HasValue && usage.CompletionTokens.GetValueOrDefault() == 0 && usage.PromptTokens.HasValue) - { - // Use specialized embedding cost for prompt tokens (cost is per million tokens) - calculatedCost += (usage.PromptTokens.Value * modelCost.EmbeddingCostPerMillionTokens.Value) / 1_000_000m; - } - else - { - // Calculate input token costs, accounting for cached tokens - var regularInputTokens = usage.PromptTokens.GetValueOrDefault(); - - // Handle cached input tokens (read from cache) - if (usage.CachedInputTokens.HasValue && usage.CachedInputTokens.Value > 0 && modelCost.CachedInputCostPerMillionTokens.HasValue) - { - // Subtract cached tokens from regular input tokens - regularInputTokens -= usage.CachedInputTokens.Value; - - // Add cost for cached tokens at the cached rate (cost is per million tokens) - calculatedCost += (usage.CachedInputTokens.Value * modelCost.CachedInputCostPerMillionTokens.Value) / 1_000_000m; - - _logger.LogDebug("Applied cached input token pricing for {CachedTokens} tokens at rate {CachedRate}", - usage.CachedInputTokens.Value, modelCost.CachedInputCostPerMillionTokens.Value); - } - - // Handle cache write tokens - if (usage.CachedWriteTokens.HasValue && usage.CachedWriteTokens.Value > 0 && modelCost.CachedInputWriteCostPerMillionTokens.HasValue) - { - // Cache writes are additional to regular input processing (cost is per million tokens) - calculatedCost += (usage.CachedWriteTokens.Value * modelCost.CachedInputWriteCostPerMillionTokens.Value) / 1_000_000m; - - _logger.LogDebug("Applied cache write token pricing for {WriteTokens} tokens at rate {WriteRate}", - usage.CachedWriteTokens.Value, modelCost.CachedInputWriteCostPerMillionTokens.Value); - } - - // Add cost for remaining regular input tokens (cost is per million tokens) - if (regularInputTokens > 0) - { - calculatedCost += (regularInputTokens * modelCost.InputCostPerMillionTokens) / 1_000_000m; - } - } - - // Always add completion token cost (cost is per million tokens) - if (usage.CompletionTokens.HasValue) - { - calculatedCost += (usage.CompletionTokens.Value * modelCost.OutputCostPerMillionTokens) / 1_000_000m; - } - - // Add image generation cost if applicable - if (modelCost.ImageCostPerImage.HasValue && usage.ImageCount.HasValue) - { - var imageCost = usage.ImageCount.Value * modelCost.ImageCostPerImage.Value; - - // Apply quality multiplier if available - if (!string.IsNullOrEmpty(modelCost.ImageQualityMultipliers) && - !string.IsNullOrEmpty(usage.ImageQuality)) - { - try - { - var qualityMultipliers = JsonSerializer.Deserialize>(modelCost.ImageQualityMultipliers); - if (qualityMultipliers != null && qualityMultipliers.TryGetValue(usage.ImageQuality.ToLowerInvariant(), out var multiplier)) - { - imageCost *= multiplier; - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to parse ImageQualityMultipliers for model {ModelId}", modelId); - } - } - - calculatedCost += imageCost; - } - - // Add video generation cost if applicable - if (modelCost.VideoCostPerSecond.HasValue && usage.VideoDurationSeconds.HasValue) - { - var baseCost = (decimal)usage.VideoDurationSeconds.Value * modelCost.VideoCostPerSecond.Value; - - // Apply resolution multiplier if available - if (!string.IsNullOrEmpty(modelCost.VideoResolutionMultipliers) && - !string.IsNullOrEmpty(usage.VideoResolution)) - { - try - { - var resolutionMultipliers = JsonSerializer.Deserialize>(modelCost.VideoResolutionMultipliers); - if (resolutionMultipliers != null && resolutionMultipliers.TryGetValue(usage.VideoResolution, out var multiplier)) - { - baseCost *= multiplier; - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to parse VideoResolutionMultipliers for model {ModelId}", modelId); - } - } - - calculatedCost += baseCost; - } - - // Add search unit cost if applicable - if (usage.SearchUnits.HasValue && usage.SearchUnits.Value > 0 && modelCost.CostPerSearchUnit.HasValue) - { - // Convert from per-1K-units to per-unit - var costPerUnit = modelCost.CostPerSearchUnit.Value / 1000m; - var searchCost = usage.SearchUnits.Value * costPerUnit; - calculatedCost += searchCost; - - _logger.LogDebug( - "Search cost calculation for model {ModelId}: {Units} units × ${CostPerUnit} = ${Total}", - modelId, - usage.SearchUnits.Value, - costPerUnit, - searchCost); - } - - // Add inference step cost if applicable (for image generation) - if (usage.InferenceSteps.HasValue && usage.InferenceSteps.Value > 0 && modelCost.CostPerInferenceStep.HasValue) - { - var stepCost = usage.InferenceSteps.Value * modelCost.CostPerInferenceStep.Value; - calculatedCost += stepCost; - - _logger.LogDebug( - "Inference step cost calculation for model {ModelId}: {Steps} steps × ${CostPerStep} = ${Total}", - modelId, - usage.InferenceSteps.Value, - modelCost.CostPerInferenceStep.Value, - stepCost); - } - - // Batch processing discount is now applied in the main CalculateCostAsync method for all pricing models - - _logger.LogDebug("Calculated cost for model {ModelId} with usage (Prompt: {PromptTokens}, Completion: {CompletionTokens}, CachedInput: {CachedInputTokens}, CachedWrite: {CachedWriteTokens}, Images: {ImageCount}, Video: {VideoDuration}s, SearchUnits: {SearchUnits}, InferenceSteps: {InferenceSteps}, IsBatch: {IsBatch}) is {CalculatedCost}", - modelId, usage.PromptTokens, usage.CompletionTokens, usage.CachedInputTokens ?? 0, usage.CachedWriteTokens ?? 0, usage.ImageCount ?? 0, usage.VideoDurationSeconds ?? 0, usage.SearchUnits ?? 0, usage.InferenceSteps ?? 0, usage.IsBatch ?? false, calculatedCost); - - return Task.FromResult(calculatedCost); - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/CostCalculationService.cs b/ConduitLLM.Core/Services/CostCalculationService.cs deleted file mode 100644 index e8132b2f5..000000000 --- a/ConduitLLM.Core/Services/CostCalculationService.cs +++ /dev/null @@ -1,145 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Core.Services; - -/// -/// Service implementation that calculates the cost of LLM operations based on usage data and model pricing. -/// -/// -/// -/// The CostCalculationService provides functionality to calculate the monetary cost of LLM operations -/// by combining usage data (tokens, images) with pricing information from the model cost repository. -/// -/// -/// This service supports cost calculation for different types of operations: -/// -/// -/// Text generation (prompt and completion tokens) -/// Embeddings (vector representations) -/// Image generation -/// -/// -/// Cost calculation is an essential component for budget management, usage tracking, -/// and providing accurate billing information to users of the system. -/// -/// -public partial class CostCalculationService : ICostCalculationService -{ - private readonly IModelCostService _modelCostService; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The service for retrieving model cost information. - /// The logger for recording diagnostic information. - /// Thrown when modelCostService or logger is null. - public CostCalculationService(IModelCostService modelCostService, ILogger logger) - { - _modelCostService = modelCostService ?? throw new ArgumentNullException(nameof(modelCostService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// - /// - /// This implementation performs cost calculation using the following logic: - /// - /// - /// Retrieves the cost information for the specified model - /// Validates input parameters and handles edge cases - /// Determines the operation type (text generation, embedding, or image generation) - /// Applies the appropriate pricing formula based on the operation type - /// - /// - /// The service uses different calculation strategies depending on the operation type: - /// - /// - /// For text generation: (promptTokens * inputTokenCost) + (completionTokens * outputTokenCost) - /// For embeddings: promptTokens * embeddingTokenCost - /// For image generation: imageCount * imageGenerationCost - /// - /// - /// If cost information is not found for the specified model, the method returns 0. - /// - /// - public async Task CalculateCostAsync(string modelId, Usage usage, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(modelId)) - { - _logger.LogWarning("Model ID is null or empty. Cannot calculate cost."); - return 0m; - } - - if (usage == null) - { - _logger.LogWarning("Usage data is null for model {ModelId}. Cannot calculate cost.", modelId); - return 0m; - } - - var modelCost = await _modelCostService.GetCostForModelAsync(modelId, cancellationToken); - - if (modelCost == null) - { - _logger.LogWarning("Cost information not found for model {ModelId}. Returning 0 cost.", modelId); - return 0m; - } - - decimal calculatedCost = 0m; - - // Handle polymorphic pricing models - switch (modelCost.PricingModel) - { - case PricingModel.Standard: - calculatedCost = await CalculateStandardCostAsync(modelId, modelCost, usage); - break; - case PricingModel.PerVideo: - calculatedCost = await CalculatePerVideoCostAsync(modelId, modelCost, usage); - break; - case PricingModel.PerSecondVideo: - calculatedCost = await CalculatePerSecondVideoCostAsync(modelId, modelCost, usage); - break; - case PricingModel.InferenceSteps: - calculatedCost = await CalculateInferenceStepsCostAsync(modelId, modelCost, usage); - break; - case PricingModel.TieredTokens: - calculatedCost = await CalculateTieredTokensCostAsync(modelId, modelCost, usage); - break; - case PricingModel.PerImage: - calculatedCost = await CalculatePerImageCostAsync(modelId, modelCost, usage); - break; - case PricingModel.PerMinuteAudio: - calculatedCost = await CalculatePerMinuteAudioCostAsync(modelId, modelCost, usage); - break; - case PricingModel.PerThousandCharacters: - calculatedCost = await CalculatePerThousandCharactersCostAsync(modelId, modelCost, usage); - break; - default: - _logger.LogWarning("Unknown pricing model {PricingModel} for model {ModelId}. Using standard calculation.", modelCost.PricingModel, modelId); - calculatedCost = await CalculateStandardCostAsync(modelId, modelCost, usage); - break; - } - - // Apply batch processing discount if applicable (works across all pricing models) - if (usage.IsBatch == true && modelCost.SupportsBatchProcessing && modelCost.BatchProcessingMultiplier.HasValue) - { - var originalCost = calculatedCost; - calculatedCost *= modelCost.BatchProcessingMultiplier!.Value; - _logger.LogDebug("Applied batch processing discount for model {ModelId}. Original cost: {OriginalCost}, Discounted cost: {DiscountedCost}, Multiplier: {Multiplier}", - modelId, originalCost, calculatedCost, modelCost.BatchProcessingMultiplier.Value); - } - - _logger.LogDebug("Calculated cost for model {ModelId} using pricing model {PricingModel} is {CalculatedCost}", - modelId, modelCost.PricingModel, calculatedCost); - - return calculatedCost; - } - - - -} diff --git a/ConduitLLM.Core/Services/DatabaseModelCapabilityService.cs b/ConduitLLM.Core/Services/DatabaseModelCapabilityService.cs deleted file mode 100644 index 33e2445d5..000000000 --- a/ConduitLLM.Core/Services/DatabaseModelCapabilityService.cs +++ /dev/null @@ -1,342 +0,0 @@ -using System.Text.Json; - -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Core.Services -{ - /// - /// Database-backed implementation of the model capability service. - /// Retrieves model capabilities from the ModelProviderMapping table. - /// - public class DatabaseModelCapabilityService : IModelCapabilityService - { - private readonly ILogger _logger; - private readonly IModelProviderMappingRepository _repository; - private readonly IMemoryCache _cache; - private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(5); - private const string CacheKeyPrefix = "ModelCapability:"; - - public DatabaseModelCapabilityService( - ILogger logger, - IModelProviderMappingRepository repository, - IMemoryCache cache) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _repository = repository ?? throw new ArgumentNullException(nameof(repository)); - _cache = cache ?? throw new ArgumentNullException(nameof(cache)); - } - - /// - public async Task SupportsVisionAsync(string model) - { - var cacheKey = $"{CacheKeyPrefix}Vision:{model}"; - if (_cache.TryGetValue(cacheKey, out var cached)) - { - return cached; - } - - try - { - var mapping = await GetMappingByModelNameAsync(model); - var result = mapping?.SupportsVision ?? false; - _cache.Set(cacheKey, result, _cacheExpiration); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking vision capability for model {Model}", model); - return false; - } - } - - /// - 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) - { - var cacheKey = $"{CacheKeyPrefix}VideoGeneration:{model}"; - if (_cache.TryGetValue(cacheKey, out var cached)) - { - return cached; - } - - try - { - var mapping = await GetMappingByModelNameAsync(model); - var result = mapping?.SupportsVideoGeneration ?? false; - _cache.Set(cacheKey, result, _cacheExpiration); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking video generation capability for model {Model}", model); - return false; - } - } - - /// - public async Task GetTokenizerTypeAsync(string model) - { - var cacheKey = $"{CacheKeyPrefix}Tokenizer:{model}"; - if (_cache.TryGetValue(cacheKey, out var cached)) - { - return cached; - } - - try - { - var mapping = await GetMappingByModelNameAsync(model); - var tokenizerType = mapping?.TokenizerType; - - // Default to cl100k_base if not specified - string result = tokenizerType?.ToString() ?? "Cl100KBase"; - - _cache.Set(cacheKey, result, _cacheExpiration); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting tokenizer type for model {Model}", model); - return "Cl100KBase"; // Default fallback - } - } - - /// - 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) - { - var cacheKey = $"{CacheKeyPrefix}Default:{provider}:{capabilityType}"; - if (_cache.TryGetValue(cacheKey, out var cached)) - { - return cached; - } - - try - { - var allMappings = await _repository.GetAllAsync(default); - var defaultMapping = allMappings.FirstOrDefault(m => - m.IsDefault && - m.Provider?.ProviderType.ToString().Equals(provider, StringComparison.OrdinalIgnoreCase) == true && - m.DefaultCapabilityType?.Equals(capabilityType, StringComparison.OrdinalIgnoreCase) == true); - - var result = defaultMapping?.ModelAlias; - if (result != null) - { - _cache.Set(cacheKey, result, _cacheExpiration); - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting default model for provider {Provider} and capability {Capability}", - provider, capabilityType); - return null; - } - } - - /// - public Task RefreshCacheAsync() - { - // Clear all cached entries with our prefix - // Note: IMemoryCache doesn't provide a way to clear by prefix, - // so we'll rely on the expiration timeout for now - _logger.LogInformation("Model capability cache refresh requested"); - return Task.CompletedTask; - } - - /// - /// Helper method to get a mapping by model name, checking both alias and provider model name. - /// - private async Task GetMappingByModelNameAsync(string model, CancellationToken cancellationToken = default) - { - var mapping = await _repository.GetByModelNameAsync(model, cancellationToken); - if (mapping == null) - { - // Try to find by provider model name - var allMappings = await _repository.GetAllAsync(cancellationToken); - mapping = allMappings.FirstOrDefault(m => - m.ProviderModelId.Equals(model, StringComparison.OrdinalIgnoreCase)); - } - return mapping; - } - } -} diff --git a/ConduitLLM.Core/Services/DiscoveryCacheService.cs b/ConduitLLM.Core/Services/DiscoveryCacheService.cs deleted file mode 100644 index e81404115..000000000 --- a/ConduitLLM.Core/Services/DiscoveryCacheService.cs +++ /dev/null @@ -1,330 +0,0 @@ -using System.Text.Json; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using ConduitLLM.Core.Interfaces; - -namespace ConduitLLM.Core.Services -{ - /// - /// Configuration options for discovery cache service - /// - public class DiscoveryCacheOptions - { - /// - /// Cache duration in minutes for discovery results - /// - public int CacheDurationMinutes { get; set; } = 360; // 6 hours - - /// - /// Whether caching is enabled - /// - public bool EnableCaching { get; set; } = true; - - /// - /// Whether to warm cache on startup - /// - public bool WarmCacheOnStartup { get; set; } = false; - - /// - /// Delay in seconds before starting cache warming to allow application to fully start - /// - public int WarmupStartupDelaySeconds { get; set; } = 5; - - /// - /// Whether to use distributed lock for cache warming coordination across instances - /// - public bool UseDistributedLockForWarming { get; set; } = true; - - /// - /// Timeout in seconds for acquiring distributed lock - /// - public int DistributedLockTimeoutSeconds { get; set; } = 30; - - /// - /// Priority models for cache warming - /// - public List PriorityModels { get; set; } = new() { "gpt-4", "claude-3", "gemini-pro" }; - - /// - /// Common capability filters to warm - /// - public List WarmupCapabilities { get; set; } = new() { "chat", "vision", "image_generation", "video_generation" }; - } - - /// - /// Implementation of discovery cache service - /// - public class DiscoveryCacheService : IDiscoveryCacheService - { - private readonly DiscoveryCacheOptions _options; - private readonly IDistributedCache? _distributedCache; - private readonly IMemoryCache _memoryCache; - private readonly ILogger _logger; - private readonly JsonSerializerOptions _jsonOptions; - - // Statistics tracking - private long _totalHits; - private long _totalMisses; - private DateTime? _lastInvalidation; - private DateTime? _lastWarmingTime; - - private const string CACHE_KEY_PREFIX = "discovery:models:"; - - public DiscoveryCacheService( - IOptions options, - IMemoryCache memoryCache, - ILogger logger, - IServiceProvider serviceProvider) - { - _options = options.Value; - _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - // Try to get distributed cache if available - _distributedCache = serviceProvider.GetService(); - - _jsonOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = false - }; - - if (_distributedCache == null) - { - _logger.LogInformation("Redis/Distributed cache not available, using memory cache only for discovery results"); - } - } - - public async Task GetDiscoveryResultsAsync(string cacheKey, CancellationToken cancellationToken = default) - { - if (!_options.EnableCaching) - { - return null; - } - - try - { - // Try distributed cache first (Redis) - if (_distributedCache != null) - { - var cachedJson = await _distributedCache.GetStringAsync(cacheKey, cancellationToken); - if (!string.IsNullOrEmpty(cachedJson)) - { - var result = JsonSerializer.Deserialize(cachedJson, _jsonOptions); - if (result != null) - { - Interlocked.Increment(ref _totalHits); - _logger.LogDebug("Discovery cache hit (Redis) for key: {CacheKey}", cacheKey); - return result; - } - } - } - - // Fallback to memory cache - if (_memoryCache.TryGetValue(cacheKey, out var memoryResult)) - { - Interlocked.Increment(ref _totalHits); - _logger.LogDebug("Discovery cache hit (Memory) for key: {CacheKey}", cacheKey); - return memoryResult; - } - - Interlocked.Increment(ref _totalMisses); - _logger.LogDebug("Discovery cache miss for key: {CacheKey}", cacheKey); - return null; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving discovery results from cache for key: {CacheKey}", cacheKey); - Interlocked.Increment(ref _totalMisses); - return null; - } - } - - public async Task SetDiscoveryResultsAsync(string cacheKey, DiscoveryModelsResult results, CancellationToken cancellationToken = default) - { - if (!_options.EnableCaching) - { - return; - } - - results.CachedAt = DateTime.UtcNow; - var expiration = TimeSpan.FromMinutes(_options.CacheDurationMinutes); - - try - { - // Set in distributed cache (Redis) - if (_distributedCache != null) - { - var json = JsonSerializer.Serialize(results, _jsonOptions); - await _distributedCache.SetStringAsync( - cacheKey, - json, - new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = expiration - }, - cancellationToken); - - _logger.LogDebug("Cached discovery results in Redis for key: {CacheKey} with {Count} models", cacheKey, results.Count); - } - - // Also set in memory cache as backup - _memoryCache.Set(cacheKey, results, expiration); - - _logger.LogInformation("Cached discovery results for key: {CacheKey} with {Count} models, expires in {Minutes} minutes", - cacheKey, results.Count, _options.CacheDurationMinutes); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error setting discovery results in cache for key: {CacheKey}", cacheKey); - } - } - - public async Task InvalidateAllDiscoveryAsync(CancellationToken cancellationToken = default) - { - try - { - _lastInvalidation = DateTime.UtcNow; - - // For Redis, we would need to scan and delete keys with pattern - // This is a simplified approach - in production, use SCAN with pattern matching - if (_distributedCache != null) - { - // Invalidate common patterns - var commonPatterns = new[] - { - $"{CACHE_KEY_PREFIX}all", - $"{CACHE_KEY_PREFIX}capability:chat", - $"{CACHE_KEY_PREFIX}capability:vision", - $"{CACHE_KEY_PREFIX}capability:image_generation", - $"{CACHE_KEY_PREFIX}capability:video_generation", - $"{CACHE_KEY_PREFIX}capability:audio_transcription", - $"{CACHE_KEY_PREFIX}capability:text_to_speech", - $"{CACHE_KEY_PREFIX}capability:embeddings", - $"{CACHE_KEY_PREFIX}capability:function_calling" - }; - - foreach (var pattern in commonPatterns) - { - await _distributedCache.RemoveAsync(pattern, cancellationToken); - } - } - - // Clear memory cache entries with discovery prefix - // Note: This is a simplified approach - production would use a more sophisticated key tracking - if (_memoryCache is MemoryCache mc) - { - mc.Compact(0.5); // Compact 50% to remove discovery entries - } - - _logger.LogInformation("Invalidated all discovery cache entries at {Time}", _lastInvalidation); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error invalidating all discovery cache entries"); - } - } - - public async Task InvalidatePatternAsync(string pattern, CancellationToken cancellationToken = default) - { - try - { - _lastInvalidation = DateTime.UtcNow; - - // For production Redis, you would use SCAN command with pattern - // For now, we'll handle specific patterns - if (_distributedCache != null && pattern.StartsWith(CACHE_KEY_PREFIX)) - { - // Handle wildcard patterns - if (pattern.EndsWith("*")) - { - await InvalidateAllDiscoveryAsync(cancellationToken); - } - else - { - await _distributedCache.RemoveAsync(pattern, cancellationToken); - } - } - - _logger.LogInformation("Invalidated discovery cache entries matching pattern: {Pattern}", pattern); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error invalidating discovery cache with pattern: {Pattern}", pattern); - } - } - - public async Task WarmDiscoveryCacheAsync(CancellationToken cancellationToken = default) - { - if (!_options.WarmCacheOnStartup || !_options.EnableCaching) - { - return; - } - - try - { - _lastWarmingTime = DateTime.UtcNow; - _logger.LogInformation("Starting discovery cache warming for {CapabilityCount} capabilities", - _options.WarmupCapabilities.Count); - - // Note: Actual warming would require calling the discovery service - // This is a placeholder for the warming logic - foreach (var capability in _options.WarmupCapabilities) - { - if (cancellationToken.IsCancellationRequested) - break; - - _logger.LogDebug("Would warm cache for capability: {Capability}", capability); - // In production: call discovery service and cache results - - await Task.Delay(100, cancellationToken); // Prevent overwhelming - } - - _logger.LogInformation("Discovery cache warming completed at {Time}", _lastWarmingTime); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during discovery cache warming"); - } - } - - public Task GetStatisticsAsync(CancellationToken cancellationToken = default) - { - var hits = Interlocked.Read(ref _totalHits); - var misses = Interlocked.Read(ref _totalMisses); - var total = hits + misses; - - var stats = new DiscoveryCacheStatistics - { - Hits = hits, - Misses = misses, - HitRate = total > 0 ? (double)hits / total * 100 : 0, - CachedEntries = 0, // Would require cache key scanning in production - LastInvalidation = _lastInvalidation, - LastWarmingTime = _lastWarmingTime - }; - - return Task.FromResult(stats); - } - - /// - /// Builds cache key for discovery results - /// - public static string BuildCacheKey(string? capability = null, int? virtualKeyId = null) - { - if (virtualKeyId.HasValue) - { - return capability != null - ? $"{CACHE_KEY_PREFIX}virtualkey:{virtualKeyId}:capability:{capability}" - : $"{CACHE_KEY_PREFIX}virtualkey:{virtualKeyId}"; - } - - return capability != null - ? $"{CACHE_KEY_PREFIX}capability:{capability}" - : $"{CACHE_KEY_PREFIX}all"; - } - } -} \ No newline at end of file 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/ImageGenerationOrchestrator.Helpers.cs b/ConduitLLM.Core/Services/ImageGenerationOrchestrator.Helpers.cs deleted file mode 100644 index d2631ac8f..000000000 --- a/ConduitLLM.Core/Services/ImageGenerationOrchestrator.Helpers.cs +++ /dev/null @@ -1,163 +0,0 @@ -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Models; -using ConduitLLM.Configuration; -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Image generation orchestrator - Helper methods - /// - public partial class ImageGenerationOrchestrator - { - private async Task GetModelInfoAsync(string? requestedModel, string virtualKeyHash) - { - // Get virtual key to check model access - var virtualKey = await _virtualKeyService.ValidateVirtualKeyAsync(virtualKeyHash, requestedModel); - if (virtualKey == null) - { - return null; - } - - // Model must be specified - no fallback - if (string.IsNullOrEmpty(requestedModel)) - { - _logger.LogWarning("No model specified for image generation request"); - return null; - } - - // Get model mapping - var mapping = await _modelMappingService.GetMappingByModelAliasAsync(requestedModel); - if (mapping == null) - { - return null; - } - - // Verify model supports image generation - if (!mapping.SupportsImageGeneration) - { - _logger.LogWarning("Model {Model} does not support image generation", requestedModel); - return null; - } - - // Get the provider entity - var provider = await _providerService.GetProviderByIdAsync(mapping.ProviderId); - if (provider == null) - { - _logger.LogWarning("Provider not found for ProviderId {ProviderId}", mapping.ProviderId); - return null; - } - - return new ModelInfo - { - Provider = provider, - ModelId = mapping.ProviderModelId, - ProviderId = mapping.ProviderId - }; - } - - private async Task CalculateImageGenerationCostAsync(ProviderType providerType, string model, int imageCount, CancellationToken cancellationToken) - { - // Create usage object for cost calculation - var usage = new Usage - { - ImageCount = imageCount - }; - - // Use the centralized cost calculation service - var cost = await _costCalculationService.CalculateCostAsync(model, usage, cancellationToken); - - return cost; - } - - private bool IsRetryableError(Exception ex) - { - // Determine if error is retryable - return ex switch - { - TaskCanceledException => true, - TimeoutException => true, - _ when ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) => true, - _ when ex.Message.Contains("rate limit", StringComparison.OrdinalIgnoreCase) => true, - _ when ex.Message.Contains("temporary", StringComparison.OrdinalIgnoreCase) => true, - _ => false - }; - } - - private async Task ReportProgressAsync( - string taskId, - string correlationId, - int totalImages, - Func getCompletedCount, - string? webhookUrl, - Dictionary? webhookHeaders, - CancellationToken cancellationToken) - { - var lastReportedCount = 0; - - while (!cancellationToken.IsCancellationRequested) - { - await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken); - - var currentCount = getCompletedCount(); - if (currentCount != lastReportedCount) - { - lastReportedCount = currentCount; - - await _publishEndpoint.Publish(new ImageGenerationProgress - { - TaskId = taskId, - Status = "storing", - ImagesCompleted = currentCount, - TotalImages = totalImages, - Message = $"Processed {currentCount} of {totalImages} images", - CorrelationId = correlationId ?? string.Empty - }); - - // Send webhook notification if configured - if (!string.IsNullOrEmpty(webhookUrl)) - { - var webhookPayload = new ImageProgressWebhookPayload - { - TaskId = taskId, - Status = "processing", - ImagesCompleted = currentCount, - TotalImages = totalImages, - Message = $"Processed {currentCount} of {totalImages} images" - }; - - // Publish webhook delivery event for scalable processing - await _publishEndpoint.Publish(new WebhookDeliveryRequested - { - TaskId = taskId, - TaskType = "image", - WebhookUrl = webhookUrl, - EventType = WebhookEventType.TaskProgress, - PayloadJson = ConduitLLM.Core.Helpers.WebhookPayloadHelper.SerializePayload(webhookPayload), - Headers = webhookHeaders, - CorrelationId = correlationId ?? Guid.NewGuid().ToString() - }); - } - } - - if (currentCount >= totalImages) - { - break; - } - } - } - - private class ModelInfo - { - public ConduitLLM.Configuration.Entities.Provider? Provider { get; set; } - public string ModelId { get; set; } = string.Empty; - public int ProviderId { get; set; } - - // Convenience property to get ProviderType from Provider - public ProviderType ProviderType => Provider?.ProviderType ?? ProviderType.OpenAI; - - // Convenience property to get provider name for responses - public string ProviderName => Provider?.ProviderName ?? Provider?.ProviderType.ToString() ?? "unknown"; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/ImageGenerationOrchestrator.Processing.cs b/ConduitLLM.Core/Services/ImageGenerationOrchestrator.Processing.cs deleted file mode 100644 index e6825a74b..000000000 --- a/ConduitLLM.Core/Services/ImageGenerationOrchestrator.Processing.cs +++ /dev/null @@ -1,215 +0,0 @@ -using System.Diagnostics; - -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Image generation orchestrator - Processing functionality - /// - public partial class ImageGenerationOrchestrator - { - private async Task ProcessSingleImageAsync( - ConduitLLM.Core.Models.ImageData imageData, - int index, - ImageGenerationRequested request, - ModelInfo modelInfo, - CancellationToken cancellationToken, - Action onProgress, - Action onTimingUpdate) - { - string? finalUrl = imageData.Url; - - if (!string.IsNullOrEmpty(imageData.B64Json)) - { - // Store base64 image using streaming to avoid loading entire content into memory - var metadata = new Dictionary - { - ["prompt"] = request.Request.Prompt, - ["model"] = modelInfo.ModelId, - ["provider"] = modelInfo.ProviderName - }; - - var mediaMetadata = new MediaMetadata - { - ContentType = "image/png", - FileName = $"generated_{DateTime.UtcNow:yyyyMMddHHmmss}_{index}.png", - MediaType = MediaType.Image, - CustomMetadata = metadata - }; - - // Use streaming to decode base64 - using var base64Stream = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(imageData.B64Json)); - using var decodedStream = new System.Security.Cryptography.CryptoStream( - base64Stream, - new System.Security.Cryptography.FromBase64Transform(), - System.Security.Cryptography.CryptoStreamMode.Read); - - var storageResult = await _storageService.StoreAsync(decodedStream, mediaMetadata); - finalUrl = storageResult.Url; - - // Publish MediaGenerationCompleted event for lifecycle tracking - await _publishEndpoint.Publish(new MediaGenerationCompleted - { - MediaType = MediaType.Image, - VirtualKeyId = request.VirtualKeyId, - MediaUrl = storageResult.Url, - StorageKey = storageResult.StorageKey, - FileSizeBytes = storageResult.SizeBytes, - ContentType = mediaMetadata.ContentType, - GeneratedByModel = modelInfo.ModelId, - GenerationPrompt = request.Request.Prompt, - GeneratedAt = DateTime.UtcNow, - Metadata = new Dictionary - { - ["provider"] = modelInfo.ProviderName, - ["model"] = modelInfo.ModelId, - ["index"] = index, - ["format"] = "b64_json" - }, - CorrelationId = request.CorrelationId?.ToString() ?? string.Empty - }); - } - else if (!string.IsNullOrEmpty(imageData.Url) && - (imageData.Url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || - imageData.Url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))) - { - var (url, downloadMs, storageMs) = await DownloadAndStoreImageAsync( - imageData.Url, - index, - request, - modelInfo, - cancellationToken); - finalUrl = url; - onTimingUpdate(downloadMs, storageMs); - } - - // Report progress - onProgress(); - - return new ConduitLLM.Core.Events.ImageData - { - Url = finalUrl, - B64Json = request.Request.ResponseFormat == "b64_json" ? imageData.B64Json : null, - RevisedPrompt = null, - Metadata = new Dictionary - { - ["provider"] = modelInfo.ProviderName, - ["model"] = modelInfo.ModelId, - ["index"] = index - } - }; - } - - private async Task<(string url, long downloadMs, long storageMs)> DownloadAndStoreImageAsync( - string imageUrl, - int index, - ImageGenerationRequested request, - ModelInfo modelInfo, - CancellationToken cancellationToken) - { - var downloadStopwatch = Stopwatch.StartNew(); - var storageStopwatch = new Stopwatch(); - - try - { - using var httpClient = _httpClientFactory.CreateClient(); - // Use a reasonable default timeout for image downloads - httpClient.Timeout = TimeSpan.FromSeconds(60); - - // Use streaming for better memory efficiency - using var response = await httpClient.GetAsync(imageUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - downloadStopwatch.Stop(); - - if (!response.IsSuccessStatusCode) - { - _logger.LogWarning("Failed to download image from {Url}: {StatusCode}", - imageUrl, response.StatusCode); - return (imageUrl, downloadStopwatch.ElapsedMilliseconds, 0); // Return original URL as fallback - } - - // Determine content type and extension - var contentType = "image/png"; - var extension = "png"; - - if (response.Content.Headers.ContentType != null) - { - contentType = response.Content.Headers.ContentType.MediaType ?? contentType; - extension = contentType.Split('/').LastOrDefault() ?? "png"; - if (extension == "jpeg") extension = "jpg"; - } - else if (imageUrl.Contains(".jpeg", StringComparison.OrdinalIgnoreCase) || - imageUrl.Contains(".jpg", StringComparison.OrdinalIgnoreCase)) - { - contentType = "image/jpeg"; - extension = "jpg"; - } - - var metadata = new Dictionary - { - ["prompt"] = request.Request.Prompt, - ["model"] = modelInfo.ModelId, - ["provider"] = modelInfo.ProviderName, - ["originalUrl"] = imageUrl - }; - - var mediaMetadata = new MediaMetadata - { - ContentType = contentType, - FileName = $"generated_{DateTime.UtcNow:yyyyMMddHHmmss}_{index}.{extension}", - MediaType = MediaType.Image, - CustomMetadata = metadata - }; - - // Add CreatedBy if we have virtual key info - if (request.VirtualKeyId > 0) - { - mediaMetadata.CreatedBy = request.VirtualKeyId.ToString(); - } - - // Stream directly to storage - using var imageStream = await response.Content.ReadAsStreamAsync(); - storageStopwatch.Start(); - var storageResult = await _storageService.StoreAsync(imageStream, mediaMetadata); - storageStopwatch.Stop(); - - _logger.LogInformation("Downloaded and stored image from {OriginalUrl} to {StorageUrl} (Download: {DownloadMs}ms, Storage: {StorageMs}ms)", - imageUrl, storageResult.Url, downloadStopwatch.ElapsedMilliseconds, storageStopwatch.ElapsedMilliseconds); - - // Get file size for the event - var contentLength = response.Content.Headers.ContentLength ?? 0; - - // Publish MediaGenerationCompleted event for lifecycle tracking - await _publishEndpoint.Publish(new MediaGenerationCompleted - { - MediaType = MediaType.Image, - VirtualKeyId = request.VirtualKeyId, - MediaUrl = storageResult.Url, - StorageKey = storageResult.StorageKey, - FileSizeBytes = contentLength, - ContentType = mediaMetadata.ContentType, - GeneratedByModel = modelInfo.ModelId, - GenerationPrompt = request.Request.Prompt, - GeneratedAt = DateTime.UtcNow, - Metadata = new Dictionary - { - ["provider"] = modelInfo.ProviderName, - ["model"] = modelInfo.ModelId, - ["index"] = index - }, - CorrelationId = request.CorrelationId - }); - - return (storageResult.Url, downloadStopwatch.ElapsedMilliseconds, storageStopwatch.ElapsedMilliseconds); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to download and store image from URL: {Url}", imageUrl); - return (imageUrl, downloadStopwatch.ElapsedMilliseconds, storageStopwatch.ElapsedMilliseconds); // Return original URL as fallback - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/ImageGenerationOrchestrator.cs b/ConduitLLM.Core/Services/ImageGenerationOrchestrator.cs deleted file mode 100644 index 6e0fbc7ba..000000000 --- a/ConduitLLM.Core/Services/ImageGenerationOrchestrator.cs +++ /dev/null @@ -1,406 +0,0 @@ -using System.Diagnostics; -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Validation; -using MassTransit; -using Microsoft.Extensions.Logging; - -using ConduitLLM.Configuration.Interfaces; -using IVirtualKeyService = ConduitLLM.Core.Interfaces.IVirtualKeyService; -using ConduitLLM.Core.Interfaces; -namespace ConduitLLM.Core.Services -{ - /// - /// Orchestrates image generation tasks by consuming events and managing the generation lifecycle. - /// - public partial class ImageGenerationOrchestrator : IConsumer, IConsumer - { - private readonly ILLMClientFactory _clientFactory; - private readonly IAsyncTaskService _taskService; - private readonly IMediaStorageService _storageService; - private readonly IPublishEndpoint _publishEndpoint; - private readonly IModelProviderMappingService _modelMappingService; - private readonly IVirtualKeyService _virtualKeyService; - private readonly IHttpClientFactory _httpClientFactory; - private readonly ICancellableTaskRegistry _taskRegistry; - private readonly ICostCalculationService _costCalculationService; - private readonly IProviderService _providerService; - private readonly MinimalParameterValidator _parameterValidator; - private readonly ILogger _logger; - - public ImageGenerationOrchestrator( - ILLMClientFactory clientFactory, - IAsyncTaskService taskService, - IMediaStorageService storageService, - IPublishEndpoint publishEndpoint, - IModelProviderMappingService modelMappingService, - IVirtualKeyService virtualKeyService, - IHttpClientFactory httpClientFactory, - ICancellableTaskRegistry taskRegistry, - ICostCalculationService costCalculationService, - IProviderService providerService, - MinimalParameterValidator parameterValidator, - ILogger logger) - { - _clientFactory = clientFactory; - _taskService = taskService; - _storageService = storageService; - _publishEndpoint = publishEndpoint; - _modelMappingService = modelMappingService; - _virtualKeyService = virtualKeyService; - _httpClientFactory = httpClientFactory; - _taskRegistry = taskRegistry; - _costCalculationService = costCalculationService; - _providerService = providerService; - _parameterValidator = parameterValidator; - _logger = logger; - } - - public async Task Consume(ConsumeContext context) - { - var request = context.Message; - var stopwatch = Stopwatch.StartNew(); - var downloadStopwatch = new Stopwatch(); - var storageStopwatch = new Stopwatch(); - ModelInfo? modelInfo = null; - - // Create a linked cancellation token source for this task - using var taskCts = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken); - - // Register the task for cancellation support - _taskRegistry.RegisterTask(request.TaskId, taskCts); - - try - { - _logger.LogInformation("Processing image generation task {TaskId} for prompt: {Prompt}", - request.TaskId, request.Request.Prompt); - - // Update task status to processing - await _taskService.UpdateTaskStatusAsync(request.TaskId, TaskState.Processing, cancellationToken: taskCts.Token); - - // Publish progress event - await _publishEndpoint.Publish(new ImageGenerationProgress - { - TaskId = request.TaskId, - Status = "processing", - ImagesCompleted = 0, - TotalImages = request.Request.N, - CorrelationId = request.CorrelationId - }); - - // Get provider and model info - modelInfo = await GetModelInfoAsync(request.Request.Model, request.VirtualKeyHash); - if (modelInfo == null) - { - throw new InvalidOperationException($"Model {request.Request.Model} not found or not available"); - } - - // Create LLM client - var client = _clientFactory.GetClient(modelInfo.ModelId); - - // Prepare generation request - var generationRequest = new ConduitLLM.Core.Models.ImageGenerationRequest - { - Prompt = request.Request.Prompt, - Model = modelInfo.ModelId, - N = request.Request.N, - Size = request.Request.Size, - Quality = request.Request.Quality, - Style = request.Request.Style, - ResponseFormat = request.Request.ResponseFormat ?? "url", - User = request.Request.User, - ExtensionData = request.Request.ExtensionData // Pass through any additional parameters - }; - - // Validate parameters (minimal, provider-agnostic) - _parameterValidator.ValidateImageParameters(generationRequest); - - _logger.LogInformation("Generating {Count} images with {Provider} using model {Model}", - generationRequest.N, modelInfo.Provider, modelInfo.ModelId); - - // Generate images with cancellation support - var response = await client.CreateImageAsync(generationRequest, cancellationToken: taskCts.Token); - - // Process and store images - var processedImages = new List(); - var totalImages = response.Data?.Count ?? 0; - - _logger.LogInformation("Processing {Count} images in parallel", totalImages); - - // Process images in parallel without artificial limits - var imageTasks = new Task[totalImages]; - var progressCounter = 0; - var downloadTime = 0L; - var storageTime = 0L; - - for (int i = 0; i < totalImages; i++) - { - var index = i; // Capture for closure - var imageData = response.Data![i]; - - imageTasks[i] = ProcessSingleImageAsync( - imageData, - index, - request, - modelInfo, - taskCts.Token, - () => Interlocked.Increment(ref progressCounter), - (dt, st) => - { - Interlocked.Add(ref downloadTime, dt); - Interlocked.Add(ref storageTime, st); - }); - } - - // Start progress reporting task - var progressTask = ReportProgressAsync( - request.TaskId, - request.CorrelationId, - totalImages, - () => progressCounter, - request.WebhookUrl, - request.WebhookHeaders, - taskCts.Token); - - // Wait for all images to complete - var results = await Task.WhenAll(imageTasks); - processedImages.AddRange(results); - - // Cancel progress reporting - taskCts.Token.ThrowIfCancellationRequested(); - - stopwatch.Stop(); - - // Calculate cost using the centralized cost calculation service - var cost = await CalculateImageGenerationCostAsync(modelInfo.ProviderType, modelInfo.ModelId, totalImages, taskCts.Token); - - // Update task with results - await _taskService.UpdateTaskStatusAsync( - request.TaskId, - TaskState.Completed, - progress: 100, - result: new - { - images = processedImages, - duration = stopwatch.Elapsed.TotalSeconds, - cost = cost, - provider = modelInfo.ProviderName, - model = modelInfo.ModelId - }); - - // Publish completion event - await _publishEndpoint.Publish(new ImageGenerationCompleted - { - TaskId = request.TaskId, - VirtualKeyId = request.VirtualKeyId, - Images = processedImages, - Duration = stopwatch.Elapsed, - Cost = cost, - Provider = modelInfo.ProviderName, - Model = modelInfo.ModelId, - CorrelationId = request.CorrelationId - }); - - // Send webhook notification if configured - if (!string.IsNullOrEmpty(request.WebhookUrl)) - { - var imageUrls = processedImages - .Where(img => !string.IsNullOrEmpty(img.Url)) - .Select(img => img.Url!) - .ToList(); - - var webhookPayload = new ImageCompletionWebhookPayload - { - TaskId = request.TaskId, - Status = "completed", - ImageUrls = imageUrls, - ImagesGenerated = processedImages.Count, - ImagesRequested = request.Request.N, - GenerationDurationSeconds = stopwatch.Elapsed.TotalSeconds, - Model = request.Request.Model, - Prompt = request.Request.Prompt, - Size = request.Request.Size, - ResponseFormat = request.Request.ResponseFormat ?? "url" - }; - - // Publish webhook delivery event for scalable processing - await _publishEndpoint.Publish(new WebhookDeliveryRequested - { - TaskId = request.TaskId, - TaskType = "image", - WebhookUrl = request.WebhookUrl, - EventType = WebhookEventType.TaskCompleted, - PayloadJson = ConduitLLM.Core.Helpers.WebhookPayloadHelper.SerializePayload(webhookPayload), - Headers = request.WebhookHeaders, - CorrelationId = request.CorrelationId ?? Guid.NewGuid().ToString() - }); - - _logger.LogDebug("Published webhook delivery event for completed image task {TaskId}", request.TaskId); - } - - // Update spend - if (cost > 0) - { - await _publishEndpoint.Publish(new SpendUpdateRequested - { - KeyId = request.VirtualKeyId, - Amount = cost, - RequestId = request.TaskId, - CorrelationId = request.CorrelationId?.ToString() ?? string.Empty - }); - } - - _logger.LogInformation("Completed image generation task {TaskId} in {Duration}s with {Count} images", - request.TaskId, stopwatch.Elapsed.TotalSeconds, processedImages.Count); - } - catch (OperationCanceledException) when (taskCts.Token.IsCancellationRequested) - { - _logger.LogInformation("Image generation task {TaskId} was cancelled", request.TaskId); - - stopwatch.Stop(); - - // Update task status to cancelled - await _taskService.UpdateTaskStatusAsync( - request.TaskId, - TaskState.Cancelled, - error: "Task was cancelled by user request"); - - // Send webhook notification if configured - if (!string.IsNullOrEmpty(request.WebhookUrl)) - { - var webhookPayload = new ImageCompletionWebhookPayload - { - TaskId = request.TaskId, - Status = "cancelled", - Error = "Task was cancelled by user request", - ImagesGenerated = 0, - ImagesRequested = request.Request.N, - GenerationDurationSeconds = stopwatch.Elapsed.TotalSeconds, - Model = request.Request.Model, - Prompt = request.Request.Prompt, - Size = request.Request.Size, - ResponseFormat = request.Request.ResponseFormat ?? "url" - }; - - // Publish webhook delivery event for scalable processing - await _publishEndpoint.Publish(new WebhookDeliveryRequested - { - TaskId = request.TaskId, - TaskType = "image", - WebhookUrl = request.WebhookUrl, - EventType = WebhookEventType.TaskCancelled, - PayloadJson = ConduitLLM.Core.Helpers.WebhookPayloadHelper.SerializePayload(webhookPayload), - Headers = request.WebhookHeaders, - CorrelationId = request.CorrelationId ?? Guid.NewGuid().ToString() - }); - - _logger.LogDebug("Published webhook delivery event for cancelled image task {TaskId}", request.TaskId); - } - - // Don't re-throw - cancellation is a normal completion path - } - catch (Exception ex) - { - _logger.LogError(ex, "Error processing image generation task {TaskId}", request.TaskId); - - stopwatch.Stop(); - - // Update task status - await _taskService.UpdateTaskStatusAsync( - request.TaskId, - TaskState.Failed, - error: ex.Message); - - // Publish failure event - await _publishEndpoint.Publish(new ImageGenerationFailed - { - TaskId = request.TaskId, - VirtualKeyId = request.VirtualKeyId, - Error = ex.Message, - ErrorCode = ex.GetType().Name, - Provider = request.Request.Model ?? "unknown", - IsRetryable = IsRetryableError(ex), - AttemptCount = 1, // Would need to track this properly - CorrelationId = request.CorrelationId - }); - - // Send webhook notification if configured - if (!string.IsNullOrEmpty(request.WebhookUrl)) - { - var webhookPayload = new ImageCompletionWebhookPayload - { - TaskId = request.TaskId, - Status = "failed", - Error = ex.Message, - ImagesGenerated = 0, - ImagesRequested = request.Request.N, - GenerationDurationSeconds = stopwatch.Elapsed.TotalSeconds, - Model = request.Request.Model, - Prompt = request.Request.Prompt, - Size = request.Request.Size, - ResponseFormat = request.Request.ResponseFormat ?? "url" - }; - - // Publish webhook delivery event for scalable processing - await _publishEndpoint.Publish(new WebhookDeliveryRequested - { - TaskId = request.TaskId, - TaskType = "image", - WebhookUrl = request.WebhookUrl, - EventType = WebhookEventType.TaskFailed, - PayloadJson = ConduitLLM.Core.Helpers.WebhookPayloadHelper.SerializePayload(webhookPayload), - Headers = request.WebhookHeaders, - CorrelationId = request.CorrelationId ?? Guid.NewGuid().ToString() - }); - - _logger.LogDebug("Published webhook delivery event for failed image task {TaskId}", request.TaskId); - } - - // Re-throw to let MassTransit handle retry logic - throw; - } - finally - { - // Always unregister the task from the cancellation registry - _taskRegistry.UnregisterTask(request.TaskId); - } - } - - public async Task Consume(ConsumeContext context) - { - var request = context.Message; - - try - { - _logger.LogInformation("Processing image generation cancellation for task {TaskId}", request.TaskId); - - // Signal cancellation to the running task if it exists - _taskRegistry.TryCancel(request.TaskId); - - // Update task status to cancelled - await _taskService.UpdateTaskStatusAsync( - request.TaskId, - TaskState.Cancelled, - error: request.Reason ?? "Cancelled by user request"); - - // Publish cancellation acknowledgement event - await _publishEndpoint.Publish(new ImageGenerationProgress - { - TaskId = request.TaskId, - Status = "cancelled", - ImagesCompleted = 0, - TotalImages = 0, - Message = "Task cancelled", - CorrelationId = request.CorrelationId - }); - - _logger.LogInformation("Successfully processed cancellation for image generation task {TaskId}", request.TaskId); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error processing image generation cancellation for task {TaskId}", request.TaskId); - // Don't re-throw - cancellation is best effort - } - } - } -} 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/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/VideoGenerationOrchestrator.Core.cs b/ConduitLLM.Core/Services/VideoGenerationOrchestrator.Core.cs deleted file mode 100644 index 77ade6905..000000000 --- a/ConduitLLM.Core/Services/VideoGenerationOrchestrator.Core.cs +++ /dev/null @@ -1,323 +0,0 @@ -using System.Diagnostics; -using ConduitLLM.Core.Configuration; -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Validation; -using MassTransit; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using ConduitLLM.Configuration.Interfaces; -using IVirtualKeyService = ConduitLLM.Core.Interfaces.IVirtualKeyService; -using ConduitLLM.Core.Interfaces; -namespace ConduitLLM.Core.Services -{ - /// - /// Orchestrates video generation tasks by consuming events and managing the generation lifecycle. - /// Handles async video generation workflow with progress tracking and error handling. - /// - public partial class VideoGenerationOrchestrator : - IConsumer, - IConsumer - { - private readonly ILLMClientFactory _clientFactory; - private readonly IAsyncTaskService _taskService; - private readonly IMediaStorageService _storageService; - private readonly IPublishEndpoint _publishEndpoint; - private readonly IModelProviderMappingService _modelMappingService; - private readonly IVirtualKeyService _virtualKeyService; - private readonly ICostCalculationService _costService; - private readonly ICancellableTaskRegistry _taskRegistry; - private readonly IWebhookNotificationService _webhookService; - private readonly VideoGenerationRetryConfiguration _retryConfiguration; - private readonly IHttpClientFactory _httpClientFactory; - private readonly MinimalParameterValidator _parameterValidator; - private readonly ILogger _logger; - - public VideoGenerationOrchestrator( - ILLMClientFactory clientFactory, - IAsyncTaskService taskService, - IMediaStorageService storageService, - IPublishEndpoint publishEndpoint, - IModelProviderMappingService modelMappingService, - IVirtualKeyService virtualKeyService, - ICostCalculationService costService, - ICancellableTaskRegistry taskRegistry, - IWebhookNotificationService webhookService, - IOptions retryConfiguration, - IHttpClientFactory httpClientFactory, - MinimalParameterValidator parameterValidator, - ILogger logger) - { - _clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); - _taskService = taskService ?? throw new ArgumentNullException(nameof(taskService)); - _storageService = storageService ?? throw new ArgumentNullException(nameof(storageService)); - _publishEndpoint = publishEndpoint ?? throw new ArgumentNullException(nameof(publishEndpoint)); - _modelMappingService = modelMappingService ?? throw new ArgumentNullException(nameof(modelMappingService)); - _virtualKeyService = virtualKeyService ?? throw new ArgumentNullException(nameof(virtualKeyService)); - _costService = costService ?? throw new ArgumentNullException(nameof(costService)); - _taskRegistry = taskRegistry ?? throw new ArgumentNullException(nameof(taskRegistry)); - _webhookService = webhookService ?? throw new ArgumentNullException(nameof(webhookService)); - _retryConfiguration = retryConfiguration?.Value ?? new VideoGenerationRetryConfiguration(); - _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); - _parameterValidator = parameterValidator ?? throw new ArgumentNullException(nameof(parameterValidator)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Handles video generation requested events. - /// - public async Task Consume(ConsumeContext context) - { - var request = context.Message; - var stopwatch = Stopwatch.StartNew(); - - _logger.LogInformation("VideoGenerationOrchestrator received event for request {RequestId}, IsAsync: {IsAsync}, Model: {Model}", - request.RequestId, request.IsAsync, request.Model); - - // Only process async requests in the orchestrator - if (!request.IsAsync) - { - _logger.LogDebug("Skipping synchronous video generation request {RequestId}", request.RequestId); - return; - } - - // NOTE: This method is now called by the background service worker - // The Task.Run anti-pattern has been removed - video generation now runs - // synchronously within the worker thread - - try - { - _logger.LogInformation("Processing async video generation task {RequestId} for model {Model}", - request.RequestId, request.Model); - - // Declare originalModelAlias at the beginning for proper scope - string originalModelAlias = request.Model; - - // Update task status to processing - await _taskService.UpdateTaskStatusAsync(request.RequestId, TaskState.Processing, cancellationToken: context.CancellationToken); - - // Publish VideoGenerationStarted event - await _publishEndpoint.Publish(new VideoGenerationStarted - { - RequestId = request.RequestId, - Provider = "pending", // Will be updated when provider is determined - StartedAt = DateTime.UtcNow, - EstimatedSeconds = 60, // Default estimate - CorrelationId = request.CorrelationId - }); - - // Get provider and model info - var modelInfo = await GetModelInfoAsync(request.Model, request.VirtualKeyId); - if (modelInfo == null) - { - throw new InvalidOperationException($"Model {request.Model} not found or not available"); - } - - // Get task to retrieve the actual virtual key from metadata - _logger.LogInformation("Retrieving task {TaskId} to get virtual key from metadata", request.RequestId); - var task = await _taskService.GetTaskStatusAsync(request.RequestId); - if (task == null) - { - _logger.LogError("Task {TaskId} not found", request.RequestId); - throw new InvalidOperationException($"Task {request.RequestId} not found"); - } - if (task.Metadata == null) - { - _logger.LogError("Task {TaskId} has no metadata", request.RequestId); - throw new InvalidOperationException($"Task {request.RequestId} has no metadata"); - } - - // Extract the virtual key from task metadata - string? virtualKey = null; - try - { - if (task.Metadata is TaskMetadata taskMetadata) - { - // The VirtualKey is stored in the ExtensionData dictionary - // When deserialized from JSON, the values might be JsonElement objects - - if (taskMetadata.ExtensionData != null) - { - _logger.LogDebug("ExtensionData has {Count} keys: {Keys}", - taskMetadata.ExtensionData.Count, - string.Join(", ", taskMetadata.ExtensionData.Keys)); - - // Try to get VirtualKey from ExtensionData - if (taskMetadata.ExtensionData.TryGetValue("VirtualKey", out var virtualKeyObj)) - { - // Handle different types the value might be - if (virtualKeyObj is string vk) - { - virtualKey = vk; - _logger.LogInformation("Extracted virtual key as string from ExtensionData"); - } - else if (virtualKeyObj is System.Text.Json.JsonElement jsonElement) - { - if (jsonElement.ValueKind == System.Text.Json.JsonValueKind.String) - { - virtualKey = jsonElement.GetString(); - _logger.LogInformation("Extracted virtual key as JsonElement string from ExtensionData"); - } - else - { - _logger.LogError("VirtualKey in ExtensionData is JsonElement but not a string. ValueKind: {ValueKind}", jsonElement.ValueKind); - } - } - else - { - _logger.LogError("VirtualKey in ExtensionData is of unexpected type: {Type}", virtualKeyObj?.GetType().FullName ?? "null"); - } - } - else - { - _logger.LogError("VirtualKey not found in ExtensionData. Available keys: {Keys}", - string.Join(", ", taskMetadata.ExtensionData.Keys)); - } - } - else - { - _logger.LogError("ExtensionData is null for task {TaskId}", request.RequestId); - } - - if (string.IsNullOrEmpty(virtualKey)) - { - _logger.LogError("Failed to extract virtual key from task metadata. VirtualKeyId: {VirtualKeyId}", - taskMetadata.VirtualKeyId); - throw new InvalidOperationException($"Virtual key not found in task metadata. VirtualKeyId: {taskMetadata.VirtualKeyId}"); - } - } - else - { - _logger.LogError("Task metadata is not of type TaskMetadata. Actual type: {MetadataType}", - task.Metadata?.GetType().FullName ?? "null"); - throw new InvalidOperationException($"Invalid task metadata type: {task.Metadata?.GetType().FullName ?? "null"}"); - } - } - catch (Exception ex) when (!(ex is InvalidOperationException)) - { - _logger.LogError(ex, "Failed to extract virtual key from task metadata"); - throw new InvalidOperationException("Failed to extract virtual key from task metadata", ex); - } - - // Validate virtual key using the actual key from metadata - var virtualKeyInfo = await _virtualKeyService.ValidateVirtualKeyAsync(virtualKey, request.Model); - if (virtualKeyInfo == null || !virtualKeyInfo.IsEnabled) - { - throw new UnauthorizedAccessException("Invalid or disabled virtual key"); - } - - // Check if model supports video generation - var mapping = await _modelMappingService.GetMappingByModelAliasAsync(request.Model); - if (mapping?.SupportsVideoGeneration != true) - { - throw new NotSupportedException($"Model {request.Model} does not support video generation"); - } - - // Perform actual video generation - await GenerateVideoAsync(request, modelInfo, virtualKeyInfo, stopwatch, context.CancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Video generation failed for request {RequestId}", request.RequestId); - - // Get current task status to check retry count - var taskStatus = await _taskService.GetTaskStatusAsync(request.RequestId); - var retryCount = taskStatus?.RetryCount ?? 0; - var maxRetries = _retryConfiguration.EnableRetries ? - (taskStatus?.MaxRetries ?? _retryConfiguration.MaxRetries) : 0; - var isRetryable = _retryConfiguration.EnableRetries && - IsRetryableError(ex) && retryCount < maxRetries; - - // Calculate next retry time with exponential backoff - DateTime? nextRetryAt = null; - if (isRetryable) - { - var delaySeconds = _retryConfiguration.CalculateRetryDelay(retryCount); - nextRetryAt = DateTime.UtcNow.AddSeconds(delaySeconds); - - // Update task status to pending for retry - await _taskService.UpdateTaskStatusAsync( - request.RequestId, - TaskState.Pending, - error: $"Retry {retryCount + 1}/{maxRetries} scheduled: {ex.Message}"); - } - else - { - // Update task status to failed (no more retries) - await _taskService.UpdateTaskStatusAsync( - request.RequestId, - TaskState.Failed, - error: ex.Message); - } - - // Publish failure event with retry information - await _publishEndpoint.Publish(new VideoGenerationFailed - { - RequestId = request.RequestId, - Error = ex.Message, - ErrorCode = ex.GetType().Name, - IsRetryable = isRetryable, - RetryCount = retryCount, - MaxRetries = maxRetries, - NextRetryAt = nextRetryAt, - FailedAt = DateTime.UtcNow, - CorrelationId = request.CorrelationId - }); - } - } - - /// - /// Handles video generation cancellation events. - /// - public async Task Consume(ConsumeContext context) - { - var cancellation = context.Message; - - _logger.LogInformation("Processing video generation cancellation for task {RequestId}", - cancellation.RequestId); - - try - { - // Try to cancel the running task via the registry - var cancelledViaRegistry = false; - if (_taskRegistry.TryCancel(cancellation.RequestId)) - { - cancelledViaRegistry = true; - _logger.LogInformation("Successfully cancelled running task {RequestId} via registry", - cancellation.RequestId); - } - else - { - _logger.LogDebug("Task {RequestId} not found in registry, it may have already completed", - cancellation.RequestId); - } - - // Update task status to cancelled - await _taskService.UpdateTaskStatusAsync( - cancellation.RequestId, - TaskState.Cancelled, - error: cancellation.Reason ?? "User requested cancellation"); - - // If we cancelled via registry, the task's cancellation token was triggered - // and the running operation should stop. The provider implementation - // needs to respect the cancellation token for this to work properly. - - _logger.LogInformation("Successfully updated video generation task {RequestId} status to cancelled (registry cancellation: {CancelledViaRegistry})", - cancellation.RequestId, cancelledViaRegistry); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to cancel video generation task {RequestId}", - cancellation.RequestId); - } - } - - private class ModelInfo - { - public string ModelId { get; set; } = string.Empty; - public string Provider { get; set; } = string.Empty; - public string ModelAlias { get; set; } = string.Empty; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/VideoGenerationOrchestrator.Handlers.cs b/ConduitLLM.Core/Services/VideoGenerationOrchestrator.Handlers.cs deleted file mode 100644 index b357384a1..000000000 --- a/ConduitLLM.Core/Services/VideoGenerationOrchestrator.Handlers.cs +++ /dev/null @@ -1,519 +0,0 @@ -using System.Diagnostics; - -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - public partial class VideoGenerationOrchestrator - { - /// - /// Process the video response after generation completes. - /// - private async Task ProcessVideoResponseAsync( - VideoGenerationRequested request, - VideoGenerationResponse response, - ModelInfo modelInfo, - ConduitLLM.Configuration.Entities.VirtualKey virtualKeyInfo, - Stopwatch stopwatch, - string originalModelAlias, - CancellationToken cancellationToken = default) - { - try - { - // Get the task metadata to reconstruct the request - var taskStatus = await _taskService.GetTaskStatusAsync(request.RequestId); - if (taskStatus == null) - { - throw new InvalidOperationException($"Task {request.RequestId} not found"); - } - - // Reconstruct the video generation request from metadata - VideoGenerationRequest videoRequest; - try - { - // Serialize the metadata object to JSON string first, then deserialize to JsonElement - var metadataJsonString = System.Text.Json.JsonSerializer.Serialize(taskStatus.Metadata); - var metadataJson = System.Text.Json.JsonSerializer.Deserialize(metadataJsonString); - - // Handle wrapped format from legacy async task service - System.Text.Json.JsonElement workingMetadata; - if (metadataJson.TryGetProperty("originalMetadata", out var originalMetadataElement)) - { - workingMetadata = originalMetadataElement; - } - else - { - workingMetadata = metadataJson; - } - - // Extract the Request from metadata - if (workingMetadata.TryGetProperty("Request", out var requestElement)) - { - videoRequest = System.Text.Json.JsonSerializer.Deserialize(requestElement.GetRawText()) ?? - throw new InvalidOperationException("Failed to deserialize request from metadata"); - } - else - { - // Fallback to constructing from event parameters - videoRequest = new VideoGenerationRequest - { - Model = request.Model, - Prompt = request.Prompt, - Duration = request.Parameters?.Duration, - Size = request.Parameters?.Size, - Fps = request.Parameters?.Fps, - ResponseFormat = "url" - }; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to extract request from task metadata, using fallback"); - // Fallback to constructing from event parameters - videoRequest = new VideoGenerationRequest - { - Model = request.Model, - Prompt = request.Prompt, - Duration = request.Parameters?.Duration ?? 6, - Size = request.Parameters?.Size ?? "1280x720", - Fps = request.Parameters?.Fps ?? 30, - ResponseFormat = "url" - }; - } - - // Store video in media storage - var videoUrl = response.Data?.FirstOrDefault()?.Url; - if (response.Data != null) - { - foreach (var video in response.Data) - { - // Handle base64 data - if (!string.IsNullOrEmpty(video.B64Json)) - { - // Use streaming to decode base64 without loading entire content into memory - using var base64Stream = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(video.B64Json)); - using var decodedStream = new System.Security.Cryptography.CryptoStream( - base64Stream, - new System.Security.Cryptography.FromBase64Transform(), - System.Security.Cryptography.CryptoStreamMode.Read); - - var videoMediaMetadata = new VideoMediaMetadata - { - MediaType = MediaType.Video, - ContentType = video.Metadata?.MimeType ?? "video/mp4", - FileSizeBytes = 0, // Will be set by storage service - FileName = $"video_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}.mp4", - Width = video.Metadata?.Width ?? 1280, - Height = video.Metadata?.Height ?? 720, - Duration = video.Metadata?.Duration ?? videoRequest.Duration ?? 6, - FrameRate = video.Metadata?.Fps ?? 30, - Codec = video.Metadata?.Codec ?? "h264", - Bitrate = video.Metadata?.Bitrate, - GeneratedByModel = request.Model, - GenerationPrompt = request.Prompt, - Resolution = videoRequest.Size ?? "1280x720" - }; - - // Create a progress callback - Action? progressCallback = bytesProcessed => - { - _logger.LogDebug("Video upload progress: {BytesProcessed} bytes", bytesProcessed); - }; - - var storageResult = await _storageService.StoreVideoAsync(decodedStream, videoMediaMetadata, progressCallback); - video.Url = storageResult.Url; - video.B64Json = null; // Clear base64 data after storing - videoUrl = storageResult.Url; - - // Publish MediaGenerationCompleted event for lifecycle tracking - await _publishEndpoint.Publish(new MediaGenerationCompleted - { - MediaType = MediaType.Video, - VirtualKeyId = virtualKeyInfo.Id, - MediaUrl = storageResult.Url, - StorageKey = storageResult.StorageKey, - FileSizeBytes = videoMediaMetadata.FileSizeBytes, - ContentType = videoMediaMetadata.ContentType, - GeneratedByModel = request.Model, - GenerationPrompt = request.Prompt, - GeneratedAt = DateTime.UtcNow, - Metadata = new Dictionary - { - ["width"] = videoMediaMetadata.Width, - ["height"] = videoMediaMetadata.Height, - ["duration"] = videoMediaMetadata.Duration, - ["frameRate"] = videoMediaMetadata.FrameRate, - ["resolution"] = videoMediaMetadata.Resolution - }, - CorrelationId = request.CorrelationId - }); - } - // Handle external URLs from any provider (Replicate, MiniMax, etc.) - else if (!string.IsNullOrEmpty(video.Url) && - (video.Url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || - video.Url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))) - { - _logger.LogInformation("Downloading and storing video from external URL: {Url}", video.Url); - - try - { - // Stream the video directly from the external URL to storage - using var httpClient = _httpClientFactory.CreateClient("VideoDownload"); - - // Set timeout based on provider - some providers generate videos faster than others - // Videos are typically larger than images, so we need longer timeouts - httpClient.Timeout = TimeSpan.FromMinutes(15); // Default to 15 minutes for video downloads - - // Use ResponseHeadersRead for streaming - using var videoResponse = await httpClient.GetAsync( - video.Url, - HttpCompletionOption.ResponseHeadersRead, - cancellationToken); - videoResponse.EnsureSuccessStatusCode(); - - var contentLength = videoResponse.Content.Headers.ContentLength ?? 0; - using var videoStream = await videoResponse.Content.ReadAsStreamAsync(); - - var videoMediaMetadata = new VideoMediaMetadata - { - MediaType = MediaType.Video, - ContentType = video.Metadata?.MimeType ?? videoResponse.Content.Headers.ContentType?.MediaType ?? "video/mp4", - FileSizeBytes = contentLength, - FileName = $"video_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}.mp4", - Width = video.Metadata?.Width ?? 1280, - Height = video.Metadata?.Height ?? 720, - Duration = video.Metadata?.Duration ?? videoRequest.Duration ?? 6, - FrameRate = video.Metadata?.Fps ?? 30, - Codec = video.Metadata?.Codec ?? "h264", - Bitrate = video.Metadata?.Bitrate, - GeneratedByModel = request.Model, - GenerationPrompt = request.Prompt, - Resolution = videoRequest.Size ?? "1280x720" - }; - - // Create progress callback that updates task status - Action? progressCallback = async (bytesProcessed) => - { - try - { - var percentage = contentLength > 0 - ? (int)((bytesProcessed * 100) / contentLength) - : -1; - - var progressMessage = contentLength > 0 - ? $"Uploading video: {bytesProcessed / 1024 / 1024}MB of {contentLength / 1024 / 1024}MB ({percentage}%)" - : $"Uploading video: {bytesProcessed / 1024 / 1024}MB"; - - await _taskService.UpdateTaskStatusAsync( - request.RequestId, - TaskState.Processing, - progress: percentage - ); - - _logger.LogDebug("Video upload progress: {BytesProcessed} bytes ({Percentage}%)", - bytesProcessed, percentage); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to update task progress"); - } - }; - - var originalUrl = video.Url; // Save original URL for logging - var storageResult = await _storageService.StoreVideoAsync(videoStream, videoMediaMetadata, progressCallback); - video.Url = storageResult.Url; - videoUrl = storageResult.Url; - - _logger.LogInformation("Successfully downloaded video from {ProviderUrl} and stored in CDN: {CdnUrl} (Size: {SizeMB}MB)", - originalUrl, storageResult.Url, contentLength / 1024 / 1024); - - // Publish MediaGenerationCompleted event for lifecycle tracking - await _publishEndpoint.Publish(new MediaGenerationCompleted - { - MediaType = MediaType.Video, - VirtualKeyId = virtualKeyInfo.Id, - MediaUrl = storageResult.Url, - StorageKey = storageResult.StorageKey, - FileSizeBytes = videoMediaMetadata.FileSizeBytes, - ContentType = videoMediaMetadata.ContentType, - GeneratedByModel = request.Model, - GenerationPrompt = request.Prompt, - GeneratedAt = DateTime.UtcNow, - Metadata = new Dictionary - { - ["width"] = videoMediaMetadata.Width, - ["height"] = videoMediaMetadata.Height, - ["duration"] = videoMediaMetadata.Duration, - ["frameRate"] = videoMediaMetadata.FrameRate, - ["resolution"] = videoMediaMetadata.Resolution - }, - CorrelationId = request.CorrelationId - }); - } - catch (TaskCanceledException) - { - _logger.LogInformation("Video download timed out or cancelled for URL: {Url}", video.Url); - throw; // Re-throw to handle cancellation properly - } - catch (OperationCanceledException) - { - _logger.LogInformation("Video download cancelled for URL: {Url}", video.Url); - throw; // Re-throw to handle cancellation properly - } - catch (HttpRequestException ex) - { - _logger.LogError(ex, "HTTP error downloading video from URL: {Url}. Video will use provider URL directly.", video.Url); - // Keep the original URL if download fails - user can still access video from provider - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to download and store video from external URL: {Url}. Video will use provider URL directly.", video.Url); - // Keep the original URL if download fails - user can still access video from provider - } - } - } - } - - // Calculate cost - var cost = await CalculateVideoCostAsync(request, modelInfo); - - // Update spend - await _virtualKeyService.UpdateSpendAsync(virtualKeyInfo.Id, cost); - - // Restore the original model alias in the response - if (response.Model != null) - { - response.Model = originalModelAlias; - } - - // Update task status to completed - var result = new - { - videoUrl = videoUrl, - usage = response.Usage, - model = response.Model ?? originalModelAlias, - created = response.Created, - duration = stopwatch.Elapsed.TotalSeconds - }; - - await _taskService.UpdateTaskStatusAsync( - request.RequestId, - TaskState.Completed, - result: result); - - // Publish VideoGenerationCompleted event - await _publishEndpoint.Publish(new VideoGenerationCompleted - { - RequestId = request.RequestId, - VideoUrl = videoUrl ?? string.Empty, - CompletedAt = DateTime.UtcNow, - GenerationDuration = stopwatch.Elapsed, - CorrelationId = request.CorrelationId - }); - - // Send webhook notification if configured - if (!string.IsNullOrEmpty(request.WebhookUrl)) - { - var webhookPayload = new VideoCompletionWebhookPayload - { - TaskId = request.RequestId, - Status = "completed", - VideoUrl = videoUrl, - GenerationDurationSeconds = stopwatch.Elapsed.TotalSeconds, - Model = request.Model, - Prompt = request.Prompt - }; - - // Publish webhook delivery event for scalable processing - await _publishEndpoint.Publish(new WebhookDeliveryRequested - { - TaskId = request.RequestId, - TaskType = "video", - WebhookUrl = request.WebhookUrl, - EventType = WebhookEventType.TaskCompleted, - PayloadJson = ConduitLLM.Core.Helpers.WebhookPayloadHelper.SerializePayload(webhookPayload), - Headers = request.WebhookHeaders, - CorrelationId = request.CorrelationId ?? Guid.NewGuid().ToString() - }); - - _logger.LogDebug("Published webhook delivery event for completed video task {RequestId}", request.RequestId); - } - - // Cancel progress tracking since task is complete - await _publishEndpoint.Publish(new VideoProgressTrackingCancelled - { - RequestId = request.RequestId, - VirtualKeyId = request.VirtualKeyId, - Reason = "Video generation completed successfully", - CorrelationId = request.CorrelationId ?? Guid.NewGuid().ToString() - }); - - _logger.LogInformation("Successfully completed video generation task {RequestId} in {Duration}ms", - request.RequestId, stopwatch.ElapsedMilliseconds); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to process video response for task {RequestId}", request.RequestId); - await HandleVideoGenerationFailureAsync(request, ex.Message, stopwatch); - } - } - - /// - /// Handle video generation failure. - /// - private async Task HandleVideoGenerationFailureAsync( - VideoGenerationRequested request, - string errorMessage, - Stopwatch stopwatch) - { - _logger.LogError("Video generation failed for task {RequestId}: {Error}", request.RequestId, errorMessage); - - // Get current task status to check retry count - var taskStatus = await _taskService.GetTaskStatusAsync(request.RequestId); - var retryCount = taskStatus?.RetryCount ?? 0; - var maxRetries = _retryConfiguration.EnableRetries ? - (taskStatus?.MaxRetries ?? _retryConfiguration.MaxRetries) : 0; - var isRetryable = _retryConfiguration.EnableRetries && - IsRetryableError(new Exception(errorMessage)) && retryCount < maxRetries; - - // Calculate next retry time with exponential backoff - DateTime? nextRetryAt = null; - if (isRetryable) - { - var delaySeconds = _retryConfiguration.CalculateRetryDelay(retryCount); - nextRetryAt = DateTime.UtcNow.AddSeconds(delaySeconds); - - // Update task status to pending for retry - await _taskService.UpdateTaskStatusAsync( - request.RequestId, - TaskState.Pending, - error: $"Retry {retryCount + 1}/{maxRetries} scheduled: {errorMessage}"); - - _logger.LogInformation("Scheduling retry {RetryCount}/{MaxRetries} for task {RequestId} at {NextRetryAt}", - retryCount + 1, maxRetries, request.RequestId, nextRetryAt); - } - else - { - // Update task status to failed (no more retries) - await _taskService.UpdateTaskStatusAsync( - request.RequestId, - TaskState.Failed, - error: errorMessage); - } - - // Publish VideoGenerationFailed event with retry information - await _publishEndpoint.Publish(new VideoGenerationFailed - { - RequestId = request.RequestId, - Error = errorMessage, - IsRetryable = isRetryable, - RetryCount = retryCount, - MaxRetries = maxRetries, - NextRetryAt = nextRetryAt, - FailedAt = DateTime.UtcNow, - CorrelationId = request.CorrelationId - }); - - // Cancel progress tracking since task has failed - await _publishEndpoint.Publish(new VideoProgressTrackingCancelled - { - RequestId = request.RequestId, - VirtualKeyId = request.VirtualKeyId, - Reason = $"Video generation failed: {errorMessage}", - CorrelationId = request.CorrelationId - }); - - // Send webhook notification if configured - if (!string.IsNullOrEmpty(request.WebhookUrl)) - { - var webhookPayload = new VideoCompletionWebhookPayload - { - TaskId = request.RequestId, - Status = isRetryable ? "retrying" : "failed", - Error = errorMessage, - Model = request.Model, - Prompt = request.Prompt - }; - - // Publish webhook delivery event for scalable processing - await _publishEndpoint.Publish(new WebhookDeliveryRequested - { - TaskId = request.RequestId, - TaskType = "video", - WebhookUrl = request.WebhookUrl, - EventType = WebhookEventType.TaskFailed, - PayloadJson = ConduitLLM.Core.Helpers.WebhookPayloadHelper.SerializePayload(webhookPayload), - Headers = request.WebhookHeaders, - CorrelationId = request.CorrelationId ?? Guid.NewGuid().ToString() - }); - - _logger.LogDebug("Published webhook delivery event for failed video task {RequestId}", request.RequestId); - } - } - - /// - /// Handle video generation cancellation. - /// - private async Task HandleVideoGenerationCancellationAsync( - VideoGenerationRequested request, - Stopwatch stopwatch) - { - _logger.LogInformation("Video generation cancelled for task {RequestId} after {ElapsedMs}ms", - request.RequestId, stopwatch.ElapsedMilliseconds); - - // Update task status to cancelled - await _taskService.UpdateTaskStatusAsync( - request.RequestId, - TaskState.Cancelled, - error: "Video generation was cancelled by user request"); - - // Publish VideoGenerationCancelled event - await _publishEndpoint.Publish(new VideoGenerationCancelled - { - RequestId = request.RequestId, - CancelledAt = DateTime.UtcNow, - CorrelationId = request.CorrelationId - }); - - // Cancel progress tracking since task is cancelled - await _publishEndpoint.Publish(new VideoProgressTrackingCancelled - { - RequestId = request.RequestId, - VirtualKeyId = request.VirtualKeyId, - Reason = "Video generation cancelled by user", - CorrelationId = request.CorrelationId - }); - - // Send webhook notification if configured - if (!string.IsNullOrEmpty(request.WebhookUrl)) - { - var webhookPayload = new VideoCompletionWebhookPayload - { - TaskId = request.RequestId, - Status = "cancelled", - Error = "Video generation was cancelled by user request", - Model = request.Model, - Prompt = request.Prompt - }; - - // Publish webhook delivery event for scalable processing - await _publishEndpoint.Publish(new WebhookDeliveryRequested - { - TaskId = request.RequestId, - TaskType = "video", - WebhookUrl = request.WebhookUrl, - EventType = WebhookEventType.TaskCancelled, - PayloadJson = ConduitLLM.Core.Helpers.WebhookPayloadHelper.SerializePayload(webhookPayload), - Headers = request.WebhookHeaders, - CorrelationId = request.CorrelationId ?? Guid.NewGuid().ToString() - }); - - _logger.LogDebug("Published webhook delivery event for cancelled video task {RequestId}", request.RequestId); - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/VideoGenerationOrchestrator.Processing.cs b/ConduitLLM.Core/Services/VideoGenerationOrchestrator.Processing.cs deleted file mode 100644 index 4d47058fb..000000000 --- a/ConduitLLM.Core/Services/VideoGenerationOrchestrator.Processing.cs +++ /dev/null @@ -1,342 +0,0 @@ -using System.Diagnostics; - -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - public partial class VideoGenerationOrchestrator - { - /// - /// Performs actual video generation using the appropriate provider. - /// - private async Task GenerateVideoAsync( - VideoGenerationRequested request, - ModelInfo modelInfo, - ConduitLLM.Configuration.Entities.VirtualKey virtualKeyInfo, - Stopwatch stopwatch, - CancellationToken cancellationToken) - { - try - { - // Get the task metadata to reconstruct the request - var taskStatus = await _taskService.GetTaskStatusAsync(request.RequestId); - if (taskStatus == null) - { - throw new InvalidOperationException($"Task {request.RequestId} not found"); - } - - // Declare originalModelAlias for proper scope - string originalModelAlias = request.Model; - - // Extract the request from metadata - VideoGenerationRequest? videoRequest; - try - { - if (taskStatus.Metadata is TaskMetadata taskMetadata) - { - // We already have the virtualKeyInfo from the database lookup - // No need to extract the virtual key string from metadata - _logger.LogDebug("Using VirtualKeyId {VirtualKeyId} from database for video generation", virtualKeyInfo.Id); - - // Extract request from extension data - videoRequest = null; - if (taskMetadata.ExtensionData != null) - { - // First try direct Request property - if (taskMetadata.ExtensionData.TryGetValue("Request", out var requestObj)) - { - if (requestObj is VideoGenerationRequest req) - { - videoRequest = req; - } - else if (requestObj is System.Text.Json.JsonElement jsonReq) - { - videoRequest = System.Text.Json.JsonSerializer.Deserialize(jsonReq.GetRawText()); - } - } - // Then try wrapped format with originalMetadata - else if (taskMetadata.ExtensionData.TryGetValue("originalMetadata", out var originalMetadataObj) && - originalMetadataObj is IDictionary originalMetadata && - originalMetadata.TryGetValue("Request", out var wrappedRequestObj)) - { - if (wrappedRequestObj is VideoGenerationRequest wrappedReq) - { - videoRequest = wrappedReq; - } - else if (wrappedRequestObj is System.Text.Json.JsonElement wrappedJsonReq) - { - videoRequest = System.Text.Json.JsonSerializer.Deserialize(wrappedJsonReq.GetRawText()); - } - } - } - - // Fallback to constructing from event parameters - if (videoRequest == null) - { - videoRequest = new VideoGenerationRequest - { - Model = request.Model, - Prompt = request.Prompt, - Duration = request.Parameters?.Duration, - Size = request.Parameters?.Size, - Fps = request.Parameters?.Fps, - N = 1 - }; - } - } - else - { - throw new InvalidOperationException($"Invalid task metadata type: {taskStatus.Metadata?.GetType().FullName ?? "null"}"); - } - } - catch (Exception ex) when (!(ex is InvalidOperationException)) - { - _logger.LogError(ex, "Failed to extract data from task metadata"); - throw new InvalidOperationException("Invalid task metadata format", ex); - } - - // Update the original model alias to the video request model - originalModelAlias = videoRequest.Model; - - // Update request to use the provider's model ID (already retrieved in modelInfo) - videoRequest.Model = modelInfo.ModelId; - - // Validate parameters (minimal, provider-agnostic) - _parameterValidator.ValidateVideoParameters(videoRequest); - - // Get the appropriate client for the model using the alias - var client = _clientFactory.GetClient(originalModelAlias); - if (client == null) - { - throw new NotSupportedException($"No provider available for model {originalModelAlias}"); - } - - // Check if the client supports video generation using reflection - VideoGenerationResponse response; - var clientType = client.GetType(); - - // Handle decorators by getting inner client - object clientToCheck = client; - if (clientType.FullName?.Contains("Decorator") == true || clientType.FullName?.Contains("PerformanceTracking") == true) - { - var innerClientField = clientType.GetField("_innerClient", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - if (innerClientField != null) - { - var innerClient = innerClientField.GetValue(client); - if (innerClient != null) - { - clientToCheck = innerClient; - clientType = innerClient.GetType(); - } - } - } - - // Find CreateVideoAsync method - var createVideoMethod = clientType.GetMethods() - .FirstOrDefault(m => m.Name == "CreateVideoAsync" && m.GetParameters().Length == 3); - - if (createVideoMethod != null) - { - // Create a cancellation token source for this specific task - var taskCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - // Register the task for cancellation - _taskRegistry.RegisterTask(request.RequestId, taskCts); - _logger.LogDebug("Registered task {RequestId} for cancellation", request.RequestId); - - try - { - // Start progress tracking via event-driven architecture - // NOTE: Disabled time-based tracking in favor of real progress from MiniMax - // await StartProgressTrackingAsync(request); - - // Set up progress callback if the client is MiniMax - if (clientType.Name == "MiniMaxClient") - { - // Use reflection to set the progress callback - var setCallbackMethod = clientType.GetMethod("SetVideoProgressCallback"); - if (setCallbackMethod != null) - { - Func progressCallback = async (taskId, status, progressPercentage) => - { - _logger.LogInformation("Video generation progress for {TaskId}: {Status} at {Progress}%", - taskId, status, progressPercentage); - - // Update task progress - await _taskService.UpdateTaskStatusAsync( - request.RequestId, - TaskState.Processing, - progress: progressPercentage); - - // Publish progress event - await _publishEndpoint.Publish(new VideoGenerationProgress - { - RequestId = request.RequestId, - ProgressPercentage = progressPercentage, - Status = status, - Message = $"Video generation {status.ToLowerInvariant()}", - CorrelationId = request.CorrelationId - }); - }; - - setCallbackMethod.Invoke(clientToCheck, new object[] { progressCallback }); - _logger.LogDebug("Set video progress callback for MiniMax client"); - } - } - - _logger.LogInformation("Processing video generation for task {RequestId} with cancellation support", request.RequestId); - - // Invoke video generation with the virtual key and cancellation token - // The client is already configured with the correct API key from the factory - var task = createVideoMethod.Invoke(clientToCheck, new object?[] { videoRequest, null, taskCts.Token }) as Task; - if (task != null) - { - response = await task; - - // Process the response when it completes - await ProcessVideoResponseAsync(request, response, modelInfo, virtualKeyInfo, stopwatch, originalModelAlias, taskCts.Token); - } - else - { - throw new InvalidOperationException($"CreateVideoAsync method on {clientType.Name} did not return expected Task"); - } - } - catch (OperationCanceledException) - { - _logger.LogInformation("Video generation for task {RequestId} was cancelled", request.RequestId); - await HandleVideoGenerationCancellationAsync(request, stopwatch); - } - catch (Exception ex) - { - _logger.LogError(ex, "Video generation failed for task {RequestId}", request.RequestId); - await HandleVideoGenerationFailureAsync(request, ex.Message, stopwatch); - } - finally - { - // Unregister the task when complete - _taskRegistry.UnregisterTask(request.RequestId); - } - - _logger.LogInformation("Video generation task {RequestId} completed processing", request.RequestId); - return; - } - else - { - throw new NotSupportedException($"Provider for model {request.Model} does not support video generation"); - } - } - catch (Exception ex) - { - await HandleVideoGenerationFailureAsync(request, ex.Message, stopwatch); - } - } - - /// - /// Starts progress tracking for a video generation task using event-driven architecture. - /// - private async Task StartProgressTrackingAsync(VideoGenerationRequested request) - { - // Publish initial progress check request - var progressCheck = new VideoProgressCheckRequested - { - RequestId = request.RequestId, - VirtualKeyId = request.VirtualKeyId, - ScheduledAt = DateTime.UtcNow.AddSeconds(5), // First check in 5 seconds - IntervalIndex = 0, - TotalIntervals = 5, // 10%, 30%, 50%, 70%, 90% - StartTime = DateTime.UtcNow, - CorrelationId = request.CorrelationId - }; - - await _publishEndpoint.Publish(progressCheck); - _logger.LogDebug("Initiated progress tracking for video generation {RequestId}", request.RequestId); - } - - /// - /// Gets model information from mappings or discovery service. - /// - private async Task GetModelInfoAsync(string modelAlias, string virtualKeyHash) - { - // First try to get from model mappings - var mapping = await _modelMappingService.GetMappingByModelAliasAsync(modelAlias); - - if (mapping != null) - { - return new ModelInfo - { - ModelId = mapping.ProviderModelId, - Provider = mapping.ProviderId.ToString(), - ModelAlias = mapping.ModelAlias - }; - } - - // No fallback - model must be in ModelProviderMapping - return null; - } - - /// - /// Calculates the cost for video generation. - /// - private async Task CalculateVideoCostAsync(VideoGenerationRequested request, ModelInfo modelInfo) - { - // Create usage object for video generation - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = request.Parameters?.Duration ?? 5, - VideoResolution = request.Parameters?.Size ?? "1280x720" - }; - - // Use the cost calculation service - return await _costService.CalculateCostAsync(modelInfo.ModelId, usage); - } - - private decimal GetResolutionMultiplier(string? resolution) - { - return resolution switch - { - "1920x1080" or "1080x1920" => 2.0m, - "1280x720" or "720x1280" => 1.5m, - "720x480" or "480x720" => 1.2m, - _ => 1.0m - }; - } - - private bool IsRetryableError(Exception ex) - { - // Check the exception type - var isRetryableType = ex switch - { - TimeoutException => true, - HttpRequestException => true, - TaskCanceledException => true, - System.IO.IOException => true, - System.Net.Sockets.SocketException => true, - _ => false - }; - - // Check for specific error messages that indicate transient failures - if (!isRetryableType && ex.Message != null) - { - var lowerMessage = ex.Message.ToLowerInvariant(); - isRetryableType = lowerMessage.Contains("timeout") || - lowerMessage.Contains("timed out") || - lowerMessage.Contains("connection") || - lowerMessage.Contains("network") || - lowerMessage.Contains("temporarily unavailable") || - lowerMessage.Contains("service unavailable") || - lowerMessage.Contains("too many requests") || - lowerMessage.Contains("rate limit"); - } - - return isRetryableType; - } - } -} \ No newline at end of file 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.Http/Authentication/VirtualKeyAuthenticationHandler.cs b/ConduitLLM.Http/Authentication/VirtualKeyAuthenticationHandler.cs deleted file mode 100644 index d94c4e04d..000000000 --- a/ConduitLLM.Http/Authentication/VirtualKeyAuthenticationHandler.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System.Security.Claims; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Options; -using ConduitLLM.Core.Interfaces; - -namespace ConduitLLM.Http.Authentication -{ - /// - /// Authentication handler for Virtual Key authentication in the Core API - /// - public class VirtualKeyAuthenticationHandler : AuthenticationHandler - { - private readonly IVirtualKeyService _virtualKeyService; - - /// - /// Initializes a new instance of the VirtualKeyAuthenticationHandler - /// - public VirtualKeyAuthenticationHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - IVirtualKeyService virtualKeyService) - : base(options, logger, encoder) - { - _virtualKeyService = virtualKeyService; - } - - /// - /// Handles the authentication for the request - /// - protected override async Task HandleAuthenticateAsync() - { - try - { - // Skip authentication for excluded paths - if (IsPathExcluded(Context.Request.Path)) - { - // Create an anonymous identity for excluded paths - var anonymousIdentity = new ClaimsIdentity(); - var anonymousPrincipal = new ClaimsPrincipal(anonymousIdentity); - var anonymousTicket = new AuthenticationTicket(anonymousPrincipal, Scheme.Name); - return AuthenticateResult.Success(anonymousTicket); - } - - // Extract Virtual Key from request - var virtualKey = ExtractVirtualKey(Context); - - if (string.IsNullOrEmpty(virtualKey)) - { - Logger.LogWarning("Missing Virtual Key in request to {Path} from IP {IP}", - Context.Request.Path, GetClientIpAddress(Context)); - return AuthenticateResult.Fail("Missing Virtual Key"); - } - - // Validate the Virtual Key for authentication only (no balance check) - var keyEntity = await _virtualKeyService.ValidateVirtualKeyForAuthenticationAsync(virtualKey); - if (keyEntity == null) - { - Logger.LogWarning("Invalid Virtual Key in request to {Path} from IP {IP}", - Context.Request.Path, GetClientIpAddress(Context)); - return AuthenticateResult.Fail("Invalid Virtual Key"); - } - - // Create claims for the authenticated user - var claims = new[] - { - new Claim(ClaimTypes.Name, keyEntity.KeyName ?? "Unknown"), - new Claim("VirtualKeyId", keyEntity.Id.ToString()), - new Claim("VirtualKey", virtualKey) - }; - - var identity = new ClaimsIdentity(claims, Scheme.Name); - var principal = new ClaimsPrincipal(identity); - var ticket = new AuthenticationTicket(principal, Scheme.Name); - - // Store virtual key info in HttpContext for usage tracking - Context.Items["VirtualKeyId"] = keyEntity.Id; - Context.Items["VirtualKey"] = virtualKey; - Context.Items["RequestStartTime"] = DateTime.UtcNow; - - Logger.LogDebug("Successfully authenticated Virtual Key {KeyName} for {Path}", - keyEntity.KeyName, Context.Request.Path); - - return AuthenticateResult.Success(ticket); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error during Virtual Key authentication for {Path}", Context.Request.Path); - return AuthenticateResult.Fail("Authentication error"); - } - } - - /// - /// Determines if the path should be excluded from authentication - /// - private bool IsPathExcluded(string path) - { - var excludedPaths = new[] - { - "/health", - "/health/ready", - "/health/live", - "/metrics", - "/v1/media/public" - }; - - return Array.Exists(excludedPaths, excludedPath => - path.StartsWith(excludedPath, StringComparison.OrdinalIgnoreCase)); - } - - /// - /// Extracts the Virtual Key from the request headers - /// - private string? ExtractVirtualKey(Microsoft.AspNetCore.Http.HttpContext context) - { - // For SignalR connections, check query string first - if (context.Request.Path.StartsWithSegments("/hubs")) - { - // Check for access_token in query string (standard for SignalR) - if (context.Request.Query.TryGetValue("access_token", out var accessToken)) - { - return accessToken.ToString(); - } - - // Also check for api_key in query string - if (context.Request.Query.TryGetValue("api_key", out var apiKey)) - { - return apiKey.ToString(); - } - } - - // Try Authorization header first (Bearer token) - var authHeader = context.Request.Headers["Authorization"].FirstOrDefault(); - if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) - { - return authHeader.Substring("Bearer ".Length).Trim(); - } - - // Try X-API-Key header - var apiKeyHeader = context.Request.Headers["X-API-Key"].FirstOrDefault(); - if (!string.IsNullOrEmpty(apiKeyHeader)) - { - return apiKeyHeader.Trim(); - } - - return null; - } - - /// - /// Gets the client IP address from the request - /// - private string GetClientIpAddress(Microsoft.AspNetCore.Http.HttpContext context) - { - // Check X-Forwarded-For header first (for reverse proxies) - var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); - if (!string.IsNullOrEmpty(forwardedFor)) - { - var ip = forwardedFor.Split(',').First().Trim(); - if (System.Net.IPAddress.TryParse(ip, out _)) - { - return ip; - } - } - - // Check X-Real-IP header - var realIp = context.Request.Headers["X-Real-IP"].FirstOrDefault(); - if (!string.IsNullOrEmpty(realIp) && System.Net.IPAddress.TryParse(realIp, out _)) - { - return realIp; - } - - // Fall back to direct connection IP - return context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Authentication/VirtualKeySignalRRateLimitFilter.cs b/ConduitLLM.Http/Authentication/VirtualKeySignalRRateLimitFilter.cs deleted file mode 100644 index f92098cdc..000000000 --- a/ConduitLLM.Http/Authentication/VirtualKeySignalRRateLimitFilter.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System.Collections.Concurrent; -using Microsoft.AspNetCore.SignalR; -using ConduitLLM.Http.Services; -using MassTransit; -using ConduitLLM.Core.Events; - -namespace ConduitLLM.Http.Authentication -{ - /// - /// Hub filter that applies rate limiting to SignalR connections based on virtual keys - /// - public class VirtualKeySignalRRateLimitFilter : IHubFilter - { - private readonly VirtualKeyRateLimitCache _rateLimitCache; - private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; - - // Track connection counts and request times per virtual key - private readonly ConcurrentDictionary _connectionInfo; - - private class ConnectionRateLimitInfo - { - public int ActiveConnections { get; set; } - public DateTime LastMinuteStart { get; set; } - public int RequestsThisMinute { get; set; } - public DateTime LastDayStart { get; set; } - public int RequestsToday { get; set; } - public readonly object Lock = new object(); - } - - /// - /// Initializes a new instance of VirtualKeySignalRRateLimitFilter - /// - public VirtualKeySignalRRateLimitFilter( - VirtualKeyRateLimitCache rateLimitCache, - ILogger logger, - IServiceProvider serviceProvider) - { - _rateLimitCache = rateLimitCache; - _logger = logger; - _serviceProvider = serviceProvider; - _connectionInfo = new ConcurrentDictionary(); - } - - /// - /// Called when a hub method is invoked - /// - public async ValueTask InvokeMethodAsync( - HubInvocationContext invocationContext, - Func> next) - { - var virtualKeyHash = GetVirtualKeyHash(invocationContext.Context); - - if (string.IsNullOrEmpty(virtualKeyHash)) - { - // No virtual key, allow through (authentication should have caught this) - return await next(invocationContext); - } - - // Check rate limits - var rateLimits = _rateLimitCache.GetRateLimits(virtualKeyHash); - if (rateLimits == null || (!rateLimits.RateLimitRpm.HasValue && !rateLimits.RateLimitRpd.HasValue)) - { - // No rate limits configured - return await next(invocationContext); - } - - // Get or create rate limit info - var info = _connectionInfo.GetOrAdd(virtualKeyHash, _ => new ConnectionRateLimitInfo - { - LastMinuteStart = DateTime.UtcNow, - LastDayStart = DateTime.UtcNow.Date - }); - - lock (info.Lock) - { - var now = DateTime.UtcNow; - - // Reset minute counter if needed - if (now - info.LastMinuteStart >= TimeSpan.FromMinutes(1)) - { - info.LastMinuteStart = now; - info.RequestsThisMinute = 0; - } - - // Reset daily counter if needed - if (now.Date != info.LastDayStart) - { - info.LastDayStart = now.Date; - info.RequestsToday = 0; - } - - // Check minute limit - if (rateLimits.RateLimitRpm.HasValue && info.RequestsThisMinute >= rateLimits.RateLimitRpm.Value) - { - _logger.LogWarning("Virtual Key {KeyHash} exceeded RPM limit of {Limit} for SignalR method {Method}", - virtualKeyHash, rateLimits.RateLimitRpm.Value, invocationContext.HubMethodName); - - // Publish rate limit exceeded event - PublishRateLimitExceeded(virtualKeyHash, "RPM", rateLimits.RateLimitRpm.Value, info.RequestsThisMinute, "minute", - info.LastMinuteStart.AddMinutes(1), invocationContext); - - throw new HubException("Rate limit exceeded. Please try again later."); - } - - // Check daily limit - if (rateLimits.RateLimitRpd.HasValue && info.RequestsToday >= rateLimits.RateLimitRpd.Value) - { - _logger.LogWarning("Virtual Key {KeyHash} exceeded RPD limit of {Limit} for SignalR method {Method}", - virtualKeyHash, rateLimits.RateLimitRpd.Value, invocationContext.HubMethodName); - - // Publish rate limit exceeded event - PublishRateLimitExceeded(virtualKeyHash, "RPD", rateLimits.RateLimitRpd.Value, info.RequestsToday, "day", - info.LastDayStart.AddDays(1), invocationContext); - - throw new HubException("Daily rate limit exceeded. Please try again tomorrow."); - } - - // Increment counters - info.RequestsThisMinute++; - info.RequestsToday++; - } - - return await next(invocationContext); - } - - /// - /// Called when a client connects - /// - public async Task OnConnectedAsync(HubLifetimeContext context, Func next) - { - var virtualKeyHash = GetVirtualKeyHash(context.Context); - - if (!string.IsNullOrEmpty(virtualKeyHash)) - { - var info = _connectionInfo.GetOrAdd(virtualKeyHash, _ => new ConnectionRateLimitInfo - { - LastMinuteStart = DateTime.UtcNow, - LastDayStart = DateTime.UtcNow.Date - }); - - lock (info.Lock) - { - info.ActiveConnections++; - } - - _logger.LogDebug("Virtual Key {KeyHash} connected. Active connections: {Count}", - virtualKeyHash, info.ActiveConnections); - } - - await next(context); - } - - /// - /// Called when a client disconnects - /// - public async Task OnDisconnectedAsync( - HubLifetimeContext context, - Exception? exception, - Func next) - { - var virtualKeyHash = GetVirtualKeyHash(context.Context); - - if (!string.IsNullOrEmpty(virtualKeyHash)) - { - if (_connectionInfo.TryGetValue(virtualKeyHash, out var info)) - { - lock (info.Lock) - { - info.ActiveConnections = Math.Max(0, info.ActiveConnections - 1); - - // Clean up if no active connections and no recent activity - if (info.ActiveConnections == 0 && - DateTime.UtcNow - info.LastMinuteStart > TimeSpan.FromMinutes(5)) - { - _connectionInfo.TryRemove(virtualKeyHash, out _); - } - } - - _logger.LogDebug("Virtual Key {KeyHash} disconnected. Active connections: {Count}", - virtualKeyHash, info.ActiveConnections); - } - } - - await next(context, exception); - } - - /// - /// Gets the virtual key hash from the connection context - /// - private string? GetVirtualKeyHash(HubCallerContext context) - { - // Try from Items first (set by hub filter) - if (context.Items.TryGetValue("VirtualKeyHash", out var itemValue) && itemValue is string itemHash) - { - return itemHash; - } - - // Try from User claims (set by authentication handler) - var claim = context.User?.FindFirst("VirtualKeyHash"); - return claim?.Value; - } - - /// - /// Publishes a rate limit exceeded event - /// - private void PublishRateLimitExceeded(string virtualKeyHash, string limitType, int limitValue, - int currentUsage, string timeWindow, DateTime resetsAt, HubInvocationContext context) - { - // Try to get virtual key ID from context - var virtualKeyId = 0; - if (context.Context.Items.TryGetValue("VirtualKeyId", out var keyIdObj) && keyIdObj is int keyId) - { - virtualKeyId = keyId; - } - - // Get IP address if available - var ipAddress = context.Context.GetHttpContext()?.Connection?.RemoteIpAddress?.ToString(); - - // Fire and forget - don't block the request - _ = Task.Run(async () => - { - try - { - using var scope = _serviceProvider.CreateScope(); - var publishEndpoint = scope.ServiceProvider.GetService(); - - if (publishEndpoint != null) - { - await publishEndpoint.Publish(new RateLimitExceeded - { - VirtualKeyId = virtualKeyId, - VirtualKeyHash = virtualKeyHash, - LimitType = limitType, - LimitValue = limitValue, - CurrentUsage = currentUsage, - TimeWindow = timeWindow, - ResetsAt = resetsAt, - IpAddress = ipAddress, - RequestedModel = null, // Not applicable for SignalR - CorrelationId = Guid.NewGuid().ToString() - }); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to publish RateLimitExceeded event for key {KeyHash}", virtualKeyHash); - } - }); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/ConduitLLM.Http.csproj b/ConduitLLM.Http/ConduitLLM.Http.csproj deleted file mode 100644 index 42545c24f..000000000 --- a/ConduitLLM.Http/ConduitLLM.Http.csproj +++ /dev/null @@ -1,60 +0,0 @@ - - - - net9.0 - enable - enable - true - Default - true - $(NoWarn);1591 - - - - 5000 - 5003 - http://127.0.0.1:$(HttpApiHttpPort);https://127.0.0.1:$(HttpApiHttpsPort) - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ConduitLLM.Http/Consumers/GlobalSettingCacheInvalidationHandler.cs b/ConduitLLM.Http/Consumers/GlobalSettingCacheInvalidationHandler.cs deleted file mode 100644 index 15e943038..000000000 --- a/ConduitLLM.Http/Consumers/GlobalSettingCacheInvalidationHandler.cs +++ /dev/null @@ -1,100 +0,0 @@ -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; - -using MassTransit; - -namespace ConduitLLM.Http.Consumers -{ - /// - /// Handles GlobalSettingChanged events for future cache invalidation - /// Currently logs events for monitoring until cache implementation is added - /// - public class GlobalSettingCacheInvalidationHandler : IConsumer - { - private readonly IGlobalSettingCache? _globalSettingCache; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the GlobalSettingCacheInvalidationHandler - /// - /// Optional global setting cache - /// Logger for diagnostics - public GlobalSettingCacheInvalidationHandler( - IGlobalSettingCache? globalSettingCache, - ILogger logger) - { - _globalSettingCache = globalSettingCache; - _logger = logger; - } - - /// - /// Consumes GlobalSettingChanged events and logs them for monitoring - /// - /// The consume context containing the event - public async Task Consume(ConsumeContext context) - { - var @event = context.Message; - - _logger.LogInformation( - "GlobalSettingChanged event received - SettingId: {SettingId}, Key: {SettingKey}, ChangeType: {ChangeType}", - @event.SettingId, - @event.SettingKey, - @event.ChangeType); - - // If it's an authentication key change, log it with higher importance - if (@event.SettingKey == "AuthenticationKey" || @event.SettingKey.StartsWith("Auth")) - { - _logger.LogWarning( - "Authentication setting changed - Key: {SettingKey}, ChangeType: {ChangeType}. Manual service restart may be required for immediate effect.", - @event.SettingKey, - @event.ChangeType); - } - - if (@event.ChangedProperties?.Length > 0) - { - _logger.LogDebug( - "Global setting properties changed: {ChangedProperties}", - string.Join(", ", @event.ChangedProperties)); - } - - // Invalidate cache if available - if (_globalSettingCache != null) - { - try - { - // Handle different change types - switch (@event.ChangeType) - { - case "Created": - case "Updated": - await _globalSettingCache.InvalidateSettingAsync(@event.SettingKey); - _logger.LogDebug("Global setting cache invalidated for key: {SettingKey}", @event.SettingKey); - break; - - case "Deleted": - await _globalSettingCache.InvalidateSettingAsync(@event.SettingKey); - _logger.LogDebug("Global setting cache invalidated for deleted key: {SettingKey}", @event.SettingKey); - break; - - case "BulkUpdate": - // For bulk updates, clear all settings to ensure consistency - await _globalSettingCache.ClearAllSettingsAsync(); - _logger.LogWarning("All global setting cache entries cleared due to bulk update"); - break; - } - - // If it's an auth-related setting, invalidate all auth settings - if (@event.SettingKey.StartsWith("Auth", StringComparison.OrdinalIgnoreCase)) - { - await _globalSettingCache.InvalidateAuthenticationSettingsAsync(); - _logger.LogWarning("All authentication-related cache entries invalidated due to auth setting change"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error invalidating global setting cache for key: {SettingKey}", @event.SettingKey); - } - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Consumers/MediaCleanupBatchConsumer.cs b/ConduitLLM.Http/Consumers/MediaCleanupBatchConsumer.cs deleted file mode 100644 index 03f1a22a0..000000000 --- a/ConduitLLM.Http/Consumers/MediaCleanupBatchConsumer.cs +++ /dev/null @@ -1,109 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Options; -using ConduitLLM.Core.Events; -using ConduitLLM.Configuration.Options; - -namespace ConduitLLM.Http.Consumers -{ - /// - /// Processes media cleanup batches and publishes R2 delete events. - /// Acts as an intermediary to prepare batches for storage deletion. - /// - public class MediaCleanupBatchConsumer : IConsumer - { - private readonly IPublishEndpoint _publishEndpoint; - private readonly MediaLifecycleOptions _options; - private readonly ILogger _logger; - private readonly IConfiguration _configuration; - - public MediaCleanupBatchConsumer( - IPublishEndpoint publishEndpoint, - IOptions options, - ILogger logger, - IConfiguration configuration) - { - _publishEndpoint = publishEndpoint; - _options = options.Value; - _logger = logger; - _configuration = configuration; - } - - public async Task Consume(ConsumeContext context) - { - var message = context.Message; - - _logger.LogInformation( - "Processing cleanup batch {BatchId} for group {GroupId} with {Count} items, Reason: {Reason}", - message.BatchId, message.VirtualKeyGroupId, message.BatchSize, message.CleanupReason); - - try - { - // Get R2 bucket configuration - var bucketName = _configuration["ConduitLLM:Storage:S3:BucketName"] ?? "conduit-media"; - - // Validate storage keys - var validKeys = message.StorageKeys - .Where(key => !string.IsNullOrWhiteSpace(key)) - .Distinct() - .ToList(); - - if (validKeys.Count != message.StorageKeys.Count) - { - _logger.LogWarning( - "Batch {BatchId} contained {Invalid} invalid storage keys out of {Total}", - message.BatchId, - message.StorageKeys.Count - validKeys.Count, - message.StorageKeys.Count); - } - - if (!validKeys.Any()) - { - _logger.LogWarning( - "Batch {BatchId} contained no valid storage keys", - message.BatchId); - return; - } - - // Create R2 delete request - var r2DeleteRequest = new R2BatchDeleteRequested( - bucketName, - validKeys, - message.VirtualKeyGroupId, - message.BatchId); - - if (_options.DryRunMode) - { - _logger.LogInformation( - "[DRY RUN] Would publish R2 delete request for {Count} objects in bucket {Bucket}", - validKeys.Count, bucketName); - - // Log sample of keys that would be deleted - var sampleKeys = validKeys.Take(5); - foreach (var key in sampleKeys) - { - _logger.LogDebug("[DRY RUN] Would delete: {Key}", key); - } - if (validKeys.Count > 5) - { - _logger.LogDebug("[DRY RUN] ... and {Count} more", validKeys.Count - 5); - } - } - else - { - await _publishEndpoint.Publish(r2DeleteRequest); - - _logger.LogInformation( - "Published R2 delete request for {Count} objects in bucket {Bucket}", - validKeys.Count, bucketName); - } - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error processing cleanup batch {BatchId} for group {GroupId}", - message.BatchId, message.VirtualKeyGroupId); - throw; - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Consumers/MediaCleanupScheduleConsumer.cs b/ConduitLLM.Http/Consumers/MediaCleanupScheduleConsumer.cs deleted file mode 100644 index eb0b861c8..000000000 --- a/ConduitLLM.Http/Consumers/MediaCleanupScheduleConsumer.cs +++ /dev/null @@ -1,111 +0,0 @@ -using MassTransit; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; -using ConduitLLM.Core.Events; -using ConduitLLM.Configuration.Interfaces; -using ConduitLLM.Configuration.Options; - -namespace ConduitLLM.Http.Consumers -{ - /// - /// Processes scheduled cleanup requests and triggers retention checks for all groups. - /// - public class MediaCleanupScheduleConsumer : IConsumer - { - private readonly IConfigurationDbContext _context; - private readonly IPublishEndpoint _publishEndpoint; - private readonly MediaLifecycleOptions _options; - private readonly ILogger _logger; - - public MediaCleanupScheduleConsumer( - IConfigurationDbContext context, - IPublishEndpoint publishEndpoint, - IOptions options, - ILogger logger) - { - _context = context; - _publishEndpoint = publishEndpoint; - _options = options.Value; - _logger = logger; - } - - public async Task Consume(ConsumeContext context) - { - var message = context.Message; - - _logger.LogInformation( - "Processing scheduled cleanup request from scheduler {SchedulerId} at {Time}, DryRun: {DryRun}", - message.SchedulerId, message.ScheduledAt, message.IsDryRun); - - try - { - // Get groups to process (all groups since Balance is not nullable) - IQueryable groupQuery = _context.VirtualKeyGroups - .Select(g => g.Id); - - // Filter by target groups if specified - if (message.TargetGroupIds?.Any() == true) - { - groupQuery = groupQuery.Where(g => message.TargetGroupIds.Contains(g)); - } - // Or filter by test groups if in test mode - else if (_options.TestVirtualKeyGroups.Any()) - { - groupQuery = groupQuery.Where(g => _options.TestVirtualKeyGroups.Contains(g)); - } - - var groupIds = await groupQuery.ToListAsync(); - - if (!groupIds.Any()) - { - _logger.LogInformation("No virtual key groups found to process"); - return; - } - - _logger.LogInformation( - "Found {Count} virtual key groups to process for retention checks", - groupIds.Count); - - // Publish retention check events for each group - var publishedCount = 0; - foreach (var groupId in groupIds) - { - var retentionCheck = new MediaRetentionCheckRequested( - groupId, - DateTime.UtcNow, - "Scheduled"); - - if (message.IsDryRun || _options.DryRunMode) - { - _logger.LogDebug( - "[DRY RUN] Would publish retention check for group {GroupId}", - groupId); - } - else - { - await _publishEndpoint.Publish(retentionCheck); - publishedCount++; - - _logger.LogDebug( - "Published retention check for group {GroupId}", - groupId); - } - - // Spread out events to avoid thundering herd - await Task.Delay(100); - } - - _logger.LogInformation( - "Scheduled cleanup completed. Published {Published}/{Total} retention checks", - publishedCount, groupIds.Count); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error processing scheduled cleanup request from scheduler {SchedulerId}", - message.SchedulerId); - throw; - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Consumers/MediaRetentionPolicyConsumer.cs b/ConduitLLM.Http/Consumers/MediaRetentionPolicyConsumer.cs deleted file mode 100644 index 7caf0a52b..000000000 --- a/ConduitLLM.Http/Consumers/MediaRetentionPolicyConsumer.cs +++ /dev/null @@ -1,194 +0,0 @@ -using MassTransit; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; -using ConduitLLM.Core.Events; -using ConduitLLM.Configuration.Interfaces; -using ConduitLLM.Configuration.Options; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Http.Consumers -{ - /// - /// Evaluates media retention policies for a virtual key group and identifies media for cleanup. - /// - public class MediaRetentionPolicyConsumer : IConsumer - { - private readonly IVirtualKeyGroupRepository _groupRepository; - private readonly IMediaRecordRepository _mediaRepository; - private readonly IConfigurationDbContext _context; - private readonly IPublishEndpoint _publishEndpoint; - private readonly MediaLifecycleOptions _options; - private readonly ILogger _logger; - - public MediaRetentionPolicyConsumer( - IVirtualKeyGroupRepository groupRepository, - IMediaRecordRepository mediaRepository, - IConfigurationDbContext context, - IPublishEndpoint publishEndpoint, - IOptions options, - ILogger logger) - { - _groupRepository = groupRepository; - _mediaRepository = mediaRepository; - _context = context; - _publishEndpoint = publishEndpoint; - _options = options.Value; - _logger = logger; - } - - public async Task Consume(ConsumeContext context) - { - var message = context.Message; - - _logger.LogInformation( - "Processing retention check for VirtualKeyGroup {GroupId}, Reason: {Reason}", - message.VirtualKeyGroupId, message.Reason); - - try - { - // Check if we're in test mode and should skip this group - if (_options.TestVirtualKeyGroups.Any() && - !_options.TestVirtualKeyGroups.Contains(message.VirtualKeyGroupId)) - { - _logger.LogDebug( - "Skipping group {GroupId} - not in test groups list", - message.VirtualKeyGroupId); - return; - } - - // Get VirtualKeyGroup with retention policy - var group = await _context.VirtualKeyGroups - .Include(g => g.MediaRetentionPolicy) - .FirstOrDefaultAsync(g => g.Id == message.VirtualKeyGroupId); - - if (group == null) - { - _logger.LogWarning( - "VirtualKeyGroup {GroupId} not found", - message.VirtualKeyGroupId); - return; - } - - // Get retention policy (use default if none specified) - var policy = group.MediaRetentionPolicy ?? await GetDefaultPolicyAsync(); - if (policy == null) - { - _logger.LogWarning( - "No retention policy found for group {GroupId} and no default policy exists", - message.VirtualKeyGroupId); - return; - } - - // Calculate retention days based on balance - var retentionDays = group.Balance switch - { - > 0 => policy.PositiveBalanceRetentionDays, - 0 => policy.ZeroBalanceRetentionDays, - < 0 => policy.NegativeBalanceRetentionDays - }; - - _logger.LogInformation( - "Group {GroupId} balance: {Balance:C}, retention days: {Days}", - group.Id, group.Balance, retentionDays); - - // Calculate cutoff date - var cutoffDate = DateTime.UtcNow.AddDays(-retentionDays); - - // Get all virtual keys in the group - var virtualKeyIds = await _context.VirtualKeys - .Where(vk => vk.VirtualKeyGroupId == message.VirtualKeyGroupId) - .Select(vk => vk.Id) - .ToListAsync(); - - if (!virtualKeyIds.Any()) - { - _logger.LogDebug("No virtual keys found in group {GroupId}", group.Id); - return; - } - - // Query media records that are eligible for cleanup - var mediaToDelete = await _context.MediaRecords - .Where(m => virtualKeyIds.Contains(m.VirtualKeyId)) - .Where(m => m.CreatedAt < cutoffDate) - .Where(m => !policy.RespectRecentAccess || - m.LastAccessedAt == null || - m.LastAccessedAt < DateTime.UtcNow.AddDays(-policy.RecentAccessWindowDays)) - .Select(m => new { m.StorageKey, m.SizeBytes }) - .ToListAsync(); - - if (!mediaToDelete.Any()) - { - _logger.LogInformation( - "No media eligible for cleanup in group {GroupId}", - group.Id); - return; - } - - var totalSize = mediaToDelete.Sum(m => m.SizeBytes ?? 0); - _logger.LogInformation( - "Found {Count} media files ({Size:N0} bytes) eligible for cleanup in group {GroupId}", - mediaToDelete.Count, totalSize, group.Id); - - // Check if manual approval is required - if (_options.RequireManualApprovalForLargeBatches && - mediaToDelete.Count > _options.LargeBatchThreshold) - { - _logger.LogWarning( - "Batch of {Count} files exceeds threshold of {Threshold}. Manual approval required.", - mediaToDelete.Count, _options.LargeBatchThreshold); - - // TODO: Implement manual approval workflow - // For now, skip large batches - return; - } - - // Batch media for deletion - var batches = mediaToDelete - .Select(m => m.StorageKey) - .Chunk(_options.MaxBatchSize); - - foreach (var batch in batches) - { - var cleanupBatch = new MediaCleanupBatchRequested( - message.VirtualKeyGroupId, - batch.ToList(), - message.Reason, - DateTime.UtcNow); - - if (_options.DryRunMode) - { - _logger.LogInformation( - "[DRY RUN] Would publish cleanup batch with {Count} items for group {GroupId}", - batch.Length, group.Id); - } - else - { - await _publishEndpoint.Publish(cleanupBatch); - _logger.LogInformation( - "Published cleanup batch with {Count} items for group {GroupId}", - batch.Length, group.Id); - } - - // Delay between batches to avoid overwhelming the system - if (_options.DelayBetweenBatchesMs > 0) - { - await Task.Delay(_options.DelayBetweenBatchesMs); - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error processing retention check for group {GroupId}", - message.VirtualKeyGroupId); - throw; - } - } - - private async Task GetDefaultPolicyAsync() - { - return await _context.MediaRetentionPolicies - .FirstOrDefaultAsync(p => p.IsDefault && p.IsActive); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Consumers/ModelCostCacheInvalidationHandler.cs b/ConduitLLM.Http/Consumers/ModelCostCacheInvalidationHandler.cs deleted file mode 100644 index 2c36747c3..000000000 --- a/ConduitLLM.Http/Consumers/ModelCostCacheInvalidationHandler.cs +++ /dev/null @@ -1,79 +0,0 @@ -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; - -using MassTransit; - -namespace ConduitLLM.Http.Consumers -{ - /// - /// Handles ModelCostChanged events for future cache invalidation - /// Currently logs events for monitoring until cache implementation is added - /// - public class ModelCostCacheInvalidationHandler : IConsumer - { - private readonly IModelCostCache? _modelCostCache; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the ModelCostCacheInvalidationHandler - /// - /// Optional model cost cache - /// Logger for diagnostics - public ModelCostCacheInvalidationHandler( - IModelCostCache? modelCostCache, - ILogger logger) - { - _modelCostCache = modelCostCache; - _logger = logger; - } - - /// - /// Consumes ModelCostChanged events and logs them for monitoring - /// - /// The consume context containing the event - public async Task Consume(ConsumeContext context) - { - var @event = context.Message; - - _logger.LogInformation( - "ModelCostChanged event received - ModelCostId: {ModelCostId}, CostName: {CostName}, ChangeType: {ChangeType}", - @event.ModelCostId, - @event.CostName, - @event.ChangeType); - - if (@event.ChangedProperties?.Length > 0) - { - _logger.LogDebug( - "Model cost properties changed: {ChangedProperties}", - string.Join(", ", @event.ChangedProperties)); - } - - // Log warning for cost changes that might affect billing - if (@event.ChangeType == "Updated" && - (@event.ChangedProperties?.Contains("InputCost") == true || - @event.ChangedProperties?.Contains("OutputCost") == true || - @event.ChangedProperties?.Contains("Cost") == true)) - { - _logger.LogWarning( - "Model pricing changed for cost '{CostName}'. This will affect cost calculations for new requests.", - @event.CostName); - } - - // Invalidate cache if available - if (_modelCostCache != null) - { - try - { - // Since the ModelCostChanged event structure is not fully defined yet, - // we'll do a conservative approach and clear all model costs - await _modelCostCache.ClearAllModelCostsAsync(); - _logger.LogInformation("Model cost cache cleared due to cost change event"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error invalidating model cost cache"); - } - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Consumers/NavigationStateEventConsumer.cs b/ConduitLLM.Http/Consumers/NavigationStateEventConsumer.cs deleted file mode 100644 index c02259dd6..000000000 --- a/ConduitLLM.Http/Consumers/NavigationStateEventConsumer.cs +++ /dev/null @@ -1,58 +0,0 @@ -using MassTransit; -using ConduitLLM.Core.Events; -using ConduitLLM.Http.Services; -namespace ConduitLLM.Http.Consumers -{ - /// - /// Consumes ModelMappingChanged events and pushes real-time updates through SignalR - /// - public class ModelMappingChangedNotificationConsumer : IConsumer - { - private readonly INavigationStateNotificationService _notificationService; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the ModelMappingChangedNotificationConsumer - /// - /// Navigation state notification service - /// Logger instance - public ModelMappingChangedNotificationConsumer( - INavigationStateNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Handles ModelMappingChanged events by pushing updates through SignalR - /// - /// Message context containing the event - public async Task Consume(ConsumeContext context) - { - var @event = context.Message; - - try - { - await _notificationService.NotifyModelMappingChangedAsync( - @event.MappingId, - @event.ModelAlias, - @event.ChangeType); - - _logger.LogInformation( - "Pushed real-time update for model mapping change: {ModelAlias} ({ChangeType})", - @event.ModelAlias, - @event.ChangeType); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Failed to push real-time update for model mapping change: {ModelAlias}", - @event.ModelAlias); - throw; // Re-throw to trigger MassTransit retry logic - } - } - } - - -} \ No newline at end of file diff --git a/ConduitLLM.Http/Consumers/R2BatchDeleteConsumer.cs b/ConduitLLM.Http/Consumers/R2BatchDeleteConsumer.cs deleted file mode 100644 index 8bfdecdd2..000000000 --- a/ConduitLLM.Http/Consumers/R2BatchDeleteConsumer.cs +++ /dev/null @@ -1,267 +0,0 @@ -using System.Net; -using MassTransit; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Configuration.Interfaces; -using ConduitLLM.Configuration.Options; - -namespace ConduitLLM.Http.Consumers -{ - /// - /// Handles actual R2 storage deletion with rate limiting for free tier. - /// Implements safety mechanisms and retry logic for transient failures. - /// - public class R2BatchDeleteConsumer : IConsumer - { - private readonly IMediaStorageService _storageService; - private readonly IMediaRecordRepository _mediaRepository; - private readonly IConfigurationDbContext _context; - private readonly IPublishEndpoint _publishEndpoint; - private readonly IDistributedLockService _lockService; - private readonly MediaLifecycleOptions _options; - private readonly ILogger _logger; - private static readonly SemaphoreSlim _rateLimiter = new(5, 5); // Max 5 concurrent operations - - public R2BatchDeleteConsumer( - IMediaStorageService storageService, - IMediaRecordRepository mediaRepository, - IConfigurationDbContext context, - IPublishEndpoint publishEndpoint, - IDistributedLockService lockService, - IOptions options, - ILogger logger) - { - _storageService = storageService; - _mediaRepository = mediaRepository; - _context = context; - _publishEndpoint = publishEndpoint; - _lockService = lockService; - _options = options.Value; - _logger = logger; - } - - public async Task Consume(ConsumeContext context) - { - var message = context.Message; - - _logger.LogInformation( - "Processing R2 delete batch {BatchId} for group {GroupId} with {Count} objects", - message.BatchId, message.VirtualKeyGroupId, message.ObjectCount); - - // Check monthly budget for free tier - var monthlyDeletes = await GetMonthlyDeleteCountAsync(); - if (monthlyDeletes >= _options.MonthlyDeleteBudget) - { - _logger.LogWarning( - "Monthly delete budget exceeded: {Count}/{Budget}. Deferring batch {BatchId}", - monthlyDeletes, _options.MonthlyDeleteBudget, message.BatchId); - - // Defer for 7 days (until next month) - await context.Defer(TimeSpan.FromDays(7)); - return; - } - - // Rate limiting for free tier - await _rateLimiter.WaitAsync(); - try - { - // Distributed lock to prevent duplicate processing - var lockKey = $"media:cleanup:{message.VirtualKeyGroupId}:{message.BatchId}"; - using var lockHandle = await _lockService.AcquireLockAsync( - lockKey, - TimeSpan.FromMinutes(5), - context.CancellationToken); - - if (lockHandle == null) - { - _logger.LogWarning( - "Could not acquire lock for batch {BatchId}. Another instance may be processing it.", - message.BatchId); - return; - } - - await ProcessBatchDeletionAsync(message, context); - } - finally - { - _rateLimiter.Release(); - } - } - - private async Task ProcessBatchDeletionAsync( - R2BatchDeleteRequested message, - ConsumeContext context) - { - var successfulDeletes = new List(); - var failedDeletes = new List<(string Key, string Error)>(); - long totalBytesFreed = 0; - - foreach (var storageKey in message.StorageKeys) - { - try - { - if (_options.DryRunMode) - { - _logger.LogDebug( - "[DRY RUN] Would delete object: {Key} from bucket: {Bucket}", - storageKey, message.BucketName); - successfulDeletes.Add(storageKey); - - // Simulate size for dry run - var mediaRecord = await _context.MediaRecords - .FirstOrDefaultAsync(m => m.StorageKey == storageKey); - if (mediaRecord != null) - { - totalBytesFreed += mediaRecord.SizeBytes ?? 0; - } - } - else - { - // Get media record for size tracking - var mediaRecord = await _context.MediaRecords - .FirstOrDefaultAsync(m => m.StorageKey == storageKey); - - // Attempt deletion from R2 - var deleted = await DeleteFromStorageAsync(storageKey); - - if (deleted) - { - successfulDeletes.Add(storageKey); - - if (mediaRecord != null) - { - totalBytesFreed += mediaRecord.SizeBytes ?? 0; - - // Delete from database - await _mediaRepository.DeleteAsync(mediaRecord.Id); - - _logger.LogDebug( - "Deleted media record {Id} with storage key {Key}", - mediaRecord.Id, storageKey); - } - } - else - { - failedDeletes.Add((storageKey, "Storage deletion failed")); - } - } - - // Small delay between deletions to avoid rate limits - await Task.Delay(100); - } - catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests) - { - _logger.LogWarning( - "R2 rate limit hit while deleting {Key}. Backing off.", - storageKey); - - // Put failed items back in the queue with delay - var remainingKeys = message.StorageKeys - .Skip(successfulDeletes.Count + failedDeletes.Count) - .ToList(); - - if (remainingKeys.Any()) - { - remainingKeys.Add(storageKey); // Add current key - - var retryBatch = new R2BatchDeleteRequested( - message.BucketName, - remainingKeys, - message.VirtualKeyGroupId, - message.BatchId); - - await context.Defer(TimeSpan.FromMinutes(5)); - } - - break; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Failed to delete object {Key} from bucket {Bucket}", - storageKey, message.BucketName); - - failedDeletes.Add((storageKey, ex.Message)); - } - } - - // Publish success event if any deletions succeeded - if (successfulDeletes.Any()) - { - var deletedEvent = new MediaDeleted( - successfulDeletes, - message.VirtualKeyGroupId, - totalBytesFreed, - DateTime.UtcNow); - - await context.Publish(deletedEvent); - - _logger.LogInformation( - "Successfully deleted {Success}/{Total} objects, freed {Bytes:N0} bytes", - successfulDeletes.Count, - message.ObjectCount, - totalBytesFreed); - - // Update monthly counter - await IncrementMonthlyDeleteCountAsync(successfulDeletes.Count); - } - - // Handle partial failures - if (failedDeletes.Any()) - { - _logger.LogWarning( - "Failed to delete {Count} objects in batch {BatchId}", - failedDeletes.Count, message.BatchId); - - // TODO: Implement poison queue for persistent failures - foreach (var (key, error) in failedDeletes.Take(5)) - { - _logger.LogError("Failed to delete {Key}: {Error}", key, error); - } - } - } - - private async Task DeleteFromStorageAsync(string storageKey) - { - try - { - // Use a timeout for R2 operations - using var cts = new CancellationTokenSource( - TimeSpan.FromSeconds(_options.R2OperationTimeoutSeconds)); - - await _storageService.DeleteAsync(storageKey); - return true; - } - catch (OperationCanceledException) - { - _logger.LogWarning( - "R2 delete operation timed out for key: {Key}", - storageKey); - return false; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error deleting object {Key} from storage", - storageKey); - return false; - } - } - - private async Task GetMonthlyDeleteCountAsync() - { - // TODO: Implement proper monthly counter using Redis or database - // For now, return 0 to allow operations - return await Task.FromResult(0); - } - - private async Task IncrementMonthlyDeleteCountAsync(int count) - { - // TODO: Implement proper monthly counter increment - // This should use Redis with expiry at end of month - await Task.CompletedTask; - } - } -} \ No newline at end of file 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/ChatController.cs b/ConduitLLM.Http/Controllers/ChatController.cs deleted file mode 100644 index f42247758..000000000 --- a/ConduitLLM.Http/Controllers/ChatController.cs +++ /dev/null @@ -1,281 +0,0 @@ -using System.Text; -using System.Text.Json; - -using ConduitLLM.Configuration; -using ConduitLLM.Core; -using ConduitLLM.Core.Controllers; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Services; -using ConduitLLM.Http.Services; - -using MassTransit; - -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using ConduitLLM.Http.Authorization; - -namespace ConduitLLM.Http.Controllers -{ - /// - /// Handles chat completion requests following OpenAI's API format. - /// - [ApiController] - [Route("v1/chat")] - [Authorize(AuthenticationSchemes = "VirtualKey,EphemeralKey")] - [RequireBalance] - [Tags("Chat")] - public class ChatController : EventPublishingControllerBase - { - private readonly Conduit _conduit; - private readonly ILogger _logger; - private readonly ConduitLLM.Configuration.Interfaces.IModelProviderMappingService _modelMappingService; - private readonly IOptions _settings; - private readonly JsonSerializerOptions _jsonSerializerOptions; - private readonly IUsageEstimationService? _usageEstimationService; - - public ChatController( - Conduit conduit, - ILogger logger, - ConduitLLM.Configuration.Interfaces.IModelProviderMappingService modelMappingService, - IOptions settings, - JsonSerializerOptions jsonSerializerOptions, - IPublishEndpoint publishEndpoint, - IUsageEstimationService? usageEstimationService = null) : base(publishEndpoint, logger) - { - _conduit = conduit ?? throw new ArgumentNullException(nameof(conduit)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _modelMappingService = modelMappingService ?? throw new ArgumentNullException(nameof(modelMappingService)); - _settings = settings ?? throw new ArgumentNullException(nameof(settings)); - _jsonSerializerOptions = jsonSerializerOptions ?? throw new ArgumentNullException(nameof(jsonSerializerOptions)); - _usageEstimationService = usageEstimationService; - } - - /// - /// Creates a chat completion. - /// - /// The chat completion request. - /// Cancellation token. - /// A chat completion response or a stream of server-sent events. - [HttpPost("completions")] - [ProducesResponseType(typeof(ChatCompletionResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(OpenAIErrorResponse), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(OpenAIErrorResponse), StatusCodes.Status500InternalServerError)] - public async Task CreateChatCompletion( - [FromBody] ChatCompletionRequest request, - CancellationToken cancellationToken = default) - { - _logger.LogInformation("Received /v1/chat/completions request for model: {Model}", request.Model); - - // Store streaming flag for middleware - HttpContext.Items["IsStreamingRequest"] = request.Stream == true; - - // 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); - } - - try - { - // Non-streaming path - if (request.Stream != true) - { - _logger.LogInformation("Handling non-streaming request."); - var response = await _conduit.CreateChatCompletionAsync(request, null, cancellationToken); - return Ok(response); - } - else - { - _logger.LogInformation("Handling streaming request."); - - // Disable response buffering for true streaming - var bufferingFeature = HttpContext.Features.Get(); - bufferingFeature?.DisableBuffering(); - - // Use enhanced SSE writer for performance metrics support - var response = HttpContext.Response; - var sseWriter = response.CreateEnhancedSSEWriter(_jsonSerializerOptions); - - // Create metrics collector if performance tracking is enabled - StreamingMetricsCollector? metricsCollector = null; - - if (_settings.Value.PerformanceTracking?.Enabled == true && _settings.Value.PerformanceTracking.TrackStreamingMetrics) - { - _logger.LogInformation("Performance tracking enabled for streaming request"); - var requestId = Guid.NewGuid().ToString(); - response.Headers["X-Request-ID"] = requestId; - - // Get provider info for metrics from model mapping service - var modelMapping = await _modelMappingService.GetMappingByModelAliasAsync(request.Model); - // Use provider ID for metrics since it's the stable identifier - var providerId = modelMapping?.ProviderId.ToString() ?? "unknown"; - - _logger.LogInformation("Creating StreamingMetricsCollector for model {Model}, provider {Provider}", request.Model, providerId); - metricsCollector = new StreamingMetricsCollector( - requestId, - request.Model, - providerId); - } - else - { - _logger.LogInformation("Performance tracking disabled for streaming request. Enabled: {Enabled}, TrackStreaming: {TrackStreaming}", - _settings.Value.PerformanceTracking?.Enabled, - _settings.Value.PerformanceTracking?.TrackStreamingMetrics); - } - - try - { - ConduitLLM.Core.Models.Usage? streamingUsage = null; - string? streamingModel = null; - - // Accumulate content for usage estimation if needed - var contentAccumulator = new StringBuilder(); - - var chunkCount = 0; - var firstChunkTime = DateTime.UtcNow; - - await foreach (var chunk in _conduit.StreamChatCompletionAsync(request, null, cancellationToken)) - { - chunkCount++; - if (chunkCount == 1) - { - _logger.LogInformation("First chunk received at {Time}ms", (DateTime.UtcNow - firstChunkTime).TotalMilliseconds); - } - - // Accumulate content from chunks for potential usage estimation - if (chunk.Choices != null) - { - foreach (var choice in chunk.Choices) - { - if (!string.IsNullOrEmpty(choice.Delta?.Content)) - { - contentAccumulator.Append(choice.Delta.Content); - } - } - } - - // Check for usage data in chunk (comes in final chunk for OpenAI-compatible APIs) - if (chunk.Usage != null) - { - streamingUsage = chunk.Usage; - streamingModel = chunk.Model ?? request.Model; - _logger.LogDebug("Captured streaming usage data: {Usage}", JsonSerializer.Serialize(streamingUsage)); - } - - // Write content event - await sseWriter.WriteContentEventAsync(chunk); - - // Track metrics if enabled - if (metricsCollector != null && chunk?.Choices?.Count == 0) - { - var hasContent = chunk.Choices.Any(c => !string.IsNullOrEmpty(c.Delta?.Content)); - if (hasContent) - { - if (metricsCollector.GetMetrics().TimeToFirstTokenMs == null) - { - metricsCollector.RecordFirstToken(); - } - else - { - metricsCollector.RecordToken(); - } - } - - // Emit metrics periodically - if (metricsCollector.ShouldEmitMetrics()) - { - _logger.LogDebug("Emitting streaming metrics"); - await sseWriter.WriteMetricsEventAsync(metricsCollector.GetMetrics()); - } - } - } - - // Store usage data for middleware to process - if (streamingUsage != null) - { - HttpContext.Items["StreamingUsage"] = streamingUsage; - HttpContext.Items["StreamingModel"] = streamingModel; - HttpContext.Items["UsageIsEstimated"] = false; - } - else if (_usageEstimationService != null && contentAccumulator.Length > 0) - { - // No usage data from provider, estimate it to prevent revenue loss - _logger.LogWarning("No usage data received from provider for streaming response, estimating usage for model {Model}", request.Model); - - try - { - var estimatedUsage = await _usageEstimationService.EstimateUsageFromStreamingResponseAsync( - streamingModel ?? request.Model, - request.Messages, - contentAccumulator.ToString(), - cancellationToken); - - HttpContext.Items["StreamingUsage"] = estimatedUsage; - HttpContext.Items["StreamingModel"] = streamingModel ?? request.Model; - HttpContext.Items["UsageIsEstimated"] = true; - - _logger.LogInformation( - "Successfully estimated usage for streaming response: Prompt={PromptTokens}, Completion={CompletionTokens}, Total={TotalTokens}", - estimatedUsage.PromptTokens, estimatedUsage.CompletionTokens, estimatedUsage.TotalTokens); - } - catch (Exception estEx) - { - _logger.LogError(estEx, "Failed to estimate usage for streaming response"); - // Don't throw - we've already sent the response to the user - // The middleware will log this as a billing failure - } - } - else if (contentAccumulator.Length == 0) - { - _logger.LogWarning("No content accumulated from streaming response, cannot estimate usage"); - } - - // Write final metrics if tracking is enabled - if (metricsCollector != null) - { - var finalMetrics = metricsCollector.GetFinalMetrics(); - await sseWriter.WriteFinalMetricsEventAsync(finalMetrics); - } - - // Write [DONE] to signal the end of the stream - await sseWriter.WriteDoneEventAsync(); - - _logger.LogInformation("Streaming completed: {ChunkCount} chunks over {Duration}ms", - chunkCount, (DateTime.UtcNow - firstChunkTime).TotalMilliseconds); - } - catch (Exception streamEx) - { - _logger.LogError(streamEx, "Error in stream processing"); - await sseWriter.WriteErrorEventAsync(streamEx.Message); - } - - return new EmptyResult(); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error processing request"); - return StatusCode(500, new OpenAIErrorResponse - { - Error = new OpenAIError - { - Message = ex.Message, - Type = "server_error", - Code = "internal_error" - } - }); - } - } - } -} diff --git a/ConduitLLM.Http/Controllers/DiscoveryController.cs b/ConduitLLM.Http/Controllers/DiscoveryController.cs deleted file mode 100644 index b17c6b7a0..000000000 --- a/ConduitLLM.Http/Controllers/DiscoveryController.cs +++ /dev/null @@ -1,351 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Services; -using Microsoft.AspNetCore.Authorization; -using ConduitLLM.Configuration.DTOs; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace ConduitLLM.Http.Controllers -{ - /// - /// Controller for discovering model capabilities and provider features. - /// Provides runtime discovery for virtual key holders to understand available models and their capabilities. - /// - [ApiController] - [Route("v1/discovery")] - [Authorize] - public class DiscoveryController : ControllerBase - { - private readonly IDbContextFactory _dbContextFactory; - private readonly IModelCapabilityService _modelCapabilityService; - private readonly IVirtualKeyService _virtualKeyService; - private readonly IDiscoveryCacheService _discoveryCacheService; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - public DiscoveryController( - IDbContextFactory dbContextFactory, - IModelCapabilityService modelCapabilityService, - IVirtualKeyService virtualKeyService, - IDiscoveryCacheService discoveryCacheService, - ILogger logger) - { - _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); - _modelCapabilityService = modelCapabilityService ?? throw new ArgumentNullException(nameof(modelCapabilityService)); - _virtualKeyService = virtualKeyService ?? throw new ArgumentNullException(nameof(virtualKeyService)); - _discoveryCacheService = discoveryCacheService ?? throw new ArgumentNullException(nameof(discoveryCacheService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Gets all discovered models and their capabilities for authenticated virtual keys. - /// - /// Optional capability filter (e.g., "video_generation", "vision") - /// List of models with their capabilities. - [HttpGet("models")] - public async Task GetModels([FromQuery] string? capability = null) - { - try - { - // Get virtual key from user claims - var virtualKeyValue = HttpContext.User.FindFirst("VirtualKey")?.Value; - if (string.IsNullOrEmpty(virtualKeyValue)) - { - return Unauthorized(new ErrorResponseDto("Virtual key not found")); - } - - // Validate virtual key is active - var virtualKey = await _virtualKeyService.ValidateVirtualKeyAsync(virtualKeyValue); - if (virtualKey == null) - { - return Unauthorized(new ErrorResponseDto("Invalid virtual key")); - } - - // Build cache key based on capability filter - var cacheKey = DiscoveryCacheService.BuildCacheKey(capability); - - // Try to get from cache first - var cachedResult = await _discoveryCacheService.GetDiscoveryResultsAsync(cacheKey); - if (cachedResult != null) - { - _logger.LogDebug("Returning cached discovery results for capability: {Capability}", capability ?? "all"); - return Ok(new - { - data = cachedResult.Data, - count = cachedResult.Count - }); - } - - using var context = await _dbContextFactory.CreateDbContextAsync(); - - // Get all enabled model mappings with their related data - var modelMappings = await context.ModelProviderMappings - .Include(m => m.Provider) - .Include(m => m.Model) - .ThenInclude(m => m.Series) - .Include(m => m.Model) - .ThenInclude(m => m.Capabilities) - .Where(m => m.IsEnabled && m.Provider != null && m.Provider.IsEnabled) - .ToListAsync(); - - var models = new List(); - - foreach (var mapping in modelMappings) - { - // Skip if model or capabilities are missing - if (mapping.Model?.Capabilities == null) - { - _logger.LogWarning("Model mapping {ModelAlias} has no model or capabilities data", mapping.ModelAlias); - continue; - } - - var caps = mapping.Model.Capabilities; - - // Apply capability filter if specified - if (!string.IsNullOrEmpty(capability)) - { - var capabilityKey = capability.Replace("-", "_").ToLowerInvariant(); - bool hasCapability = capabilityKey switch - { - "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, - "function_calling" => caps.SupportsFunctionCalling, - _ => false - }; - - if (!hasCapability) - { - continue; - } - } - - // TODO: Revisit supported_parameters implementation after removing ApiParameters field - // Currently commented out as we're moving to full parameter pass-through - // and ApiParameters field is being deprecated. Parameters should be derived - // from the UI-focused Parameters JSON object instead. - /* - // Parse parameters from mapping (priority) or series (fallback) - string[]? supportedParameters = null; - var parametersJson = mapping.ApiParameters ?? mapping.Model?.Series?.Parameters; - if (!string.IsNullOrEmpty(parametersJson)) - { - try - { - supportedParameters = System.Text.Json.JsonSerializer.Deserialize(parametersJson); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to parse parameters for model {ModelAlias}", mapping.ModelAlias); - } - } - */ - - models.Add(new - { - // Identity - id = mapping.ModelAlias, - provider = mapping.Provider?.ProviderType.ToString().ToLowerInvariant(), - display_name = mapping.ModelAlias, - - // Metadata - description = mapping.Model?.Description ?? string.Empty, - model_card_url = mapping.Model?.ModelCardUrl ?? string.Empty, - max_tokens = caps.MaxTokens, - tokenizer_type = caps.TokenizerType.ToString().ToLowerInvariant(), - - // Configuration - // supported_parameters = supportedParameters ?? Array.Empty(), // TODO: Re-implement based on Parameters field - - // UI Parameters from Model or Series - parameters = mapping.Model?.ModelParameters ?? mapping.Model?.Series?.Parameters ?? "{}", - - // Capabilities (flat boolean flags) - supports_chat = caps.SupportsChat, - 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 - - // TODO: Future additions to consider: - // - 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 - }); - } - - // Cache the results for future requests - var discoveryResult = new DiscoveryModelsResult - { - Data = models, - Count = models.Count, - CapabilityFilter = capability - }; - - await _discoveryCacheService.SetDiscoveryResultsAsync(cacheKey, discoveryResult); - - _logger.LogInformation("Cached discovery results for capability: {Capability} with {Count} models", - capability ?? "all", models.Count); - - return Ok(new - { - data = models, - count = models.Count - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving model discovery information"); - return StatusCode(500, new ErrorResponseDto("Failed to retrieve model discovery information")); - } - } - - /// - /// Gets all available capabilities in the system. - /// - /// List of all available capabilities. - [HttpGet("capabilities")] - public Task GetCapabilities() - { - try - { - // Return all known capabilities - var capabilities = new[] - { - "chat", - "chat_stream", - "vision", - "audio_transcription", - "text_to_speech", - "realtime_audio", - "video_generation", - "image_generation", - "embeddings", - "function_calling", - "tool_use", - "json_mode" - }; - - return Task.FromResult(Ok(new - { - capabilities = capabilities - })); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving capabilities list"); - return Task.FromResult(StatusCode(500, new ErrorResponseDto("Failed to retrieve capabilities"))); - } - } - - /// - /// Gets UI parameters for a specific model to enable dynamic UI generation. - /// - /// The model alias or identifier to get parameters for - /// JSON object containing UI parameter definitions for the model. - /// - /// This endpoint returns the UI-focused parameter definitions from the ModelSeries.Parameters field, - /// which contains JSON objects defining sliders, selects, textareas, and other UI controls. - /// This allows clients to dynamically generate appropriate UI controls without Admin API access. - /// - [HttpGet("models/{model}/parameters")] - public async Task GetModelParameters(string model) - { - try - { - // Get virtual key from user claims - var virtualKeyValue = HttpContext.User.FindFirst("VirtualKey")?.Value; - if (string.IsNullOrEmpty(virtualKeyValue)) - { - return Unauthorized(new ErrorResponseDto("Virtual key not found")); - } - - // Validate virtual key is active - var virtualKey = await _virtualKeyService.ValidateVirtualKeyAsync(virtualKeyValue); - if (virtualKey == null) - { - return Unauthorized(new ErrorResponseDto("Invalid virtual key")); - } - - using var context = await _dbContextFactory.CreateDbContextAsync(); - - // Find the model mapping by alias - var modelMapping = await context.ModelProviderMappings - .Include(m => m.Model) - .ThenInclude(m => m!.Series) - .Where(m => m.ModelAlias == model && m.IsEnabled) - .FirstOrDefaultAsync(); - - if (modelMapping == null) - { - // Try to find by Model.Id if the input is numeric - if (int.TryParse(model, out var modelId)) - { - modelMapping = await context.ModelProviderMappings - .Include(m => m.Model) - .ThenInclude(m => m!.Series) - .Where(m => m.ModelId == modelId && m.IsEnabled) - .FirstOrDefaultAsync(); - } - } - - if (modelMapping?.Model?.Series == null) - { - return NotFound(new ErrorResponseDto($"Model '{model}' not found or has no parameter information")); - } - - // Parse the Parameters JSON - object? parameters = null; - if (!string.IsNullOrEmpty(modelMapping.Model.Series.Parameters)) - { - try - { - parameters = System.Text.Json.JsonSerializer.Deserialize( - modelMapping.Model.Series.Parameters); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to parse parameters for model {Model}", model); - parameters = new { }; - } - } - - return Ok(new - { - model_id = modelMapping.ModelId, - model_alias = modelMapping.ModelAlias, - series_name = modelMapping.Model.Series.Name, - parameters = parameters ?? new { } - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving model parameters for {Model}", model); - return StatusCode(500, new ErrorResponseDto("Failed to retrieve model parameters")); - } - } - - } - - // TODO: Add audit logging for discovery requests to track which virtual keys are querying model information - // TODO: Consider adding pricing information to model discovery responses once pricing data is available in the system -} 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/MediaController.cs b/ConduitLLM.Http/Controllers/MediaController.cs deleted file mode 100644 index 0bcf15f47..000000000 --- a/ConduitLLM.Http/Controllers/MediaController.cs +++ /dev/null @@ -1,261 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; - -namespace ConduitLLM.Http.Controllers -{ - /// - /// Handles media file retrieval and serving. - /// - [ApiController] - [Route("v1/media")] - [Authorize] - public class MediaController : ControllerBase - { - private readonly IMediaStorageService _storageService; - private readonly ILogger _logger; - - public MediaController( - IMediaStorageService storageService, - ILogger logger) - { - _storageService = storageService; - _logger = logger; - } - - /// - /// Retrieves a media file by its storage key. - /// - /// The unique storage key. - /// The media file. - [HttpGet("{**storageKey}")] - [AllowAnonymous] // Media URLs should work without auth - public async Task GetMedia(string storageKey) - { - try - { - // Validate storage key - if (string.IsNullOrWhiteSpace(storageKey)) - { - return BadRequest("Invalid storage key"); - } - - // Get media info - var mediaInfo = await _storageService.GetInfoAsync(storageKey); - if (mediaInfo == null) - { - return NotFound(); - } - - // Check if this is a video and if range is requested - if (mediaInfo.MediaType == MediaType.Video && Request.Headers.ContainsKey(HeaderNames.Range)) - { - return await HandleVideoRangeRequest(storageKey, mediaInfo); - } - - // Get media stream for non-video or non-range requests - var stream = await _storageService.GetStreamAsync(storageKey); - if (stream == null) - { - return NotFound(); - } - - // Set cache headers for performance - Response.Headers["Cache-Control"] = "public, max-age=3600"; // 1 hour - Response.Headers["ETag"] = $"\"{storageKey}\""; - - // Add CORS headers for video playback - if (mediaInfo.MediaType == MediaType.Video) - { - Response.Headers["Accept-Ranges"] = "bytes"; - Response.Headers["Access-Control-Allow-Origin"] = "*"; - Response.Headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS"; - Response.Headers["Access-Control-Allow-Headers"] = "Range"; - } - - // Return file with proper content type - return File(stream, mediaInfo.ContentType, enableRangeProcessing: true); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving media with key {StorageKey}", storageKey); - return StatusCode(500, "An error occurred while retrieving the media"); - } - } - - /// - /// Gets metadata information about a media file. - /// - /// The unique storage key. - /// Media metadata. - [HttpGet("info/{**storageKey}")] - public async Task GetMediaInfo(string storageKey) - { - try - { - var mediaInfo = await _storageService.GetInfoAsync(storageKey); - if (mediaInfo == null) - { - return NotFound(); - } - - return Ok(mediaInfo); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving media info for key {StorageKey}", storageKey); - return StatusCode(500, "An error occurred while retrieving media information"); - } - } - - /// - /// Checks if a media file exists. - /// - /// The unique storage key. - /// True if the media exists. - [HttpHead("{**storageKey}")] - [AllowAnonymous] - public async Task CheckMediaExists(string storageKey) - { - try - { - var exists = await _storageService.ExistsAsync(storageKey); - if (!exists) - { - return NotFound(); - } - - var mediaInfo = await _storageService.GetInfoAsync(storageKey); - if (mediaInfo != null) - { - Response.Headers["Content-Type"] = mediaInfo.ContentType; - Response.Headers["Content-Length"] = mediaInfo.SizeBytes.ToString(); - } - - return Ok(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking media existence for key {StorageKey}", storageKey); - return StatusCode(500); - } - } - - /// - /// Handles HTTP range requests for video streaming. - /// - private async Task HandleVideoRangeRequest(string storageKey, MediaInfo mediaInfo) - { - try - { - var rangeHeader = Request.Headers[HeaderNames.Range].FirstOrDefault(); - if (string.IsNullOrEmpty(rangeHeader)) - { - return BadRequest("Invalid range header"); - } - - // Parse range header (e.g., "bytes=0-1023") - var range = ParseRangeHeader(rangeHeader, mediaInfo.SizeBytes); - if (range == null) - { - return StatusCode(416, "Requested Range Not Satisfiable"); // 416 Range Not Satisfiable - } - - // Get video stream with range - var rangedStream = await _storageService.GetVideoStreamAsync( - storageKey, - range.Value.Start, - range.Value.End); - - if (rangedStream == null) - { - return NotFound(); - } - - // Set response headers for partial content - Response.StatusCode = 206; // Partial Content - Response.Headers["Accept-Ranges"] = "bytes"; - Response.Headers["Content-Range"] = $"bytes {rangedStream.RangeStart}-{rangedStream.RangeEnd}/{rangedStream.TotalSize}"; - Response.Headers["Content-Length"] = rangedStream.ContentLength.ToString(); - Response.Headers["Cache-Control"] = "public, max-age=3600"; - Response.Headers["ETag"] = $"\"{storageKey}\""; - - // CORS headers for video playback - Response.Headers["Access-Control-Allow-Origin"] = "*"; - Response.Headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS"; - Response.Headers["Access-Control-Allow-Headers"] = "Range"; - - return File(rangedStream.Stream, rangedStream.ContentType); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error handling video range request for key {StorageKey}", storageKey); - return StatusCode(500, "An error occurred while streaming the video"); - } - } - - /// - /// Parses HTTP range header. - /// - private (long Start, long End)? ParseRangeHeader(string rangeHeader, long totalSize) - { - try - { - // Remove "bytes=" prefix - if (!rangeHeader.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - var rangeValue = rangeHeader.Substring(6); - var parts = rangeValue.Split('-'); - - if (parts.Length != 2) - { - return null; - } - - long start = 0; - long end = totalSize - 1; - - // Parse start - if (!string.IsNullOrEmpty(parts[0])) - { - if (!long.TryParse(parts[0], out start)) - { - return null; - } - } - - // Parse end - if (!string.IsNullOrEmpty(parts[1])) - { - if (!long.TryParse(parts[1], out end)) - { - return null; - } - } - else if (!string.IsNullOrEmpty(parts[0])) - { - // If no end specified, use a reasonable chunk size (1MB) - end = Math.Min(start + 1024 * 1024 - 1, totalSize - 1); - } - - // Validate range - if (start < 0 || start >= totalSize || end < start || end >= totalSize) - { - return null; - } - - return (start, end); - } - catch - { - return null; - } - } - } -} 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/Dockerfile b/ConduitLLM.Http/Dockerfile deleted file mode 100644 index 7bab77f62..000000000 --- a/ConduitLLM.Http/Dockerfile +++ /dev/null @@ -1,63 +0,0 @@ -# HTTP API Dockerfile - Optimized for selective cache invalidation -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build -WORKDIR /src - -# ===== CACHED LAYERS (Keep these fast) ===== -# Copy project files for dependency resolution -COPY *.sln . -COPY Directory.Build.props ./ -COPY */*.csproj ./ -RUN find . -name "*.csproj" -exec dirname {} \; | xargs -I {} mkdir -p {} - -# Copy project files to correct locations -COPY ConduitLLM.Http/*.csproj ./ConduitLLM.Http/ -COPY ConduitLLM.Configuration/*.csproj ./ConduitLLM.Configuration/ -COPY ConduitLLM.Core/*.csproj ./ConduitLLM.Core/ -COPY ConduitLLM.Security/*.csproj ./ConduitLLM.Security/ -COPY ConduitLLM.Providers/*.csproj ./ConduitLLM.Providers/ -COPY ConduitLLM.Admin/*.csproj ./ConduitLLM.Admin/ -COPY ConduitLLM.Tests/*.csproj ./ConduitLLM.Tests/ -COPY ConduitLLM.IntegrationTests/*.csproj ./ConduitLLM.IntegrationTests/ - -# Restore dependencies (cached unless project files change) -RUN dotnet restore "ConduitLLM.Http/ConduitLLM.Http.csproj" - -# ===== CACHE INVALIDATION POINT ===== -# This argument forces rebuild from here down when .NET code changes -ARG CACHEBUST=1 - -# Copy all source code (this and everything after will rebuild) -COPY . . - -# Build and publish -RUN dotnet publish "ConduitLLM.Http/ConduitLLM.Http.csproj" \ - -c Release \ - -o /app/publish \ - --no-restore - -# Runtime stage - Debian-based for reliability -FROM mcr.microsoft.com/dotnet/aspnet:9.0 -WORKDIR /app - -# Install curl for health checks -RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* - -# Copy published application -COPY --from=build /app/publish . - -# Create non-root user -RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app -USER appuser - -# Configure ASP.NET Core -ENV ASPNETCORE_URLS=http://+:8080 -ENV ASPNETCORE_ENVIRONMENT=Production -ENV DOTNET_RUNNING_IN_CONTAINER=true - -EXPOSE 8080 - -# Health check -HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ - CMD curl -f http://localhost:8080/health/ready || exit 1 - -ENTRYPOINT ["dotnet", "ConduitLLM.Http.dll"] \ No newline at end of file diff --git a/ConduitLLM.Http/EventHandlers/ModelMappingCacheInvalidationHandler.cs b/ConduitLLM.Http/EventHandlers/ModelMappingCacheInvalidationHandler.cs deleted file mode 100644 index 070d76c22..000000000 --- a/ConduitLLM.Http/EventHandlers/ModelMappingCacheInvalidationHandler.cs +++ /dev/null @@ -1,70 +0,0 @@ -using MassTransit; -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Http.Interfaces; - -namespace ConduitLLM.Http.EventHandlers -{ - /// - /// Handles ModelMappingChanged events to refresh in-memory settings and invalidate discovery cache - /// Critical for maintaining runtime configuration consistency - /// - public class ModelMappingCacheInvalidationHandler : IConsumer - { - private readonly ISettingsRefreshService _settingsRefreshService; - private readonly IDiscoveryCacheService _discoveryCacheService; - private readonly ILogger _logger; - - public ModelMappingCacheInvalidationHandler( - ISettingsRefreshService settingsRefreshService, - IDiscoveryCacheService discoveryCacheService, - ILogger logger) - { - _settingsRefreshService = settingsRefreshService ?? throw new ArgumentNullException(nameof(settingsRefreshService)); - _discoveryCacheService = discoveryCacheService ?? throw new ArgumentNullException(nameof(discoveryCacheService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Handles ModelMappingChanged events by refreshing model mappings from the database and invalidating discovery cache - /// - public async Task Consume(ConsumeContext context) - { - var @event = context.Message; - - try - { - _logger.LogInformation( - "Processing ModelMappingChanged event: {ModelAlias} ({ChangeType})", - @event.ModelAlias, - @event.ChangeType); - - // Refresh all model mappings to ensure consistency - await _settingsRefreshService.RefreshModelMappingsAsync(); - - // Invalidate discovery cache - always use direct invalidation - // Note: Batch invalidation doesn't support wildcard patterns, and we need to - // invalidate all discovery cache entries (all, capability:chat, capability:vision, etc.) - await _discoveryCacheService.InvalidateAllDiscoveryAsync(); - - _logger.LogInformation( - "Invalidated all discovery cache entries after {ChangeType} of {ModelAlias}", - @event.ChangeType, - @event.ModelAlias); - - _logger.LogInformation( - "Successfully refreshed model mappings and invalidated discovery cache after {ChangeType} of {ModelAlias}", - @event.ChangeType, - @event.ModelAlias); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Failed to refresh model mappings or invalidate cache after {ChangeType} of {ModelAlias}", - @event.ChangeType, - @event.ModelAlias); - throw; // Re-throw to trigger MassTransit retry logic - } - } - } -} \ No newline at end of file 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/Extensions/HealthMonitoringExtensions.cs b/ConduitLLM.Http/Extensions/HealthMonitoringExtensions.cs deleted file mode 100644 index 4c1208c23..000000000 --- a/ConduitLLM.Http/Extensions/HealthMonitoringExtensions.cs +++ /dev/null @@ -1,81 +0,0 @@ -using ConduitLLM.Http.Interfaces; -using ConduitLLM.Http.Services; -using ConduitLLM.Security.Interfaces; -using ConduitLLM.Security.Models; - -namespace ConduitLLM.Http.Extensions -{ - /// - /// Extension methods for configuring health monitoring services - /// - public static class HealthMonitoringExtensions - { - /// - /// Adds health monitoring services and advanced health checks - /// - public static IServiceCollection AddHealthMonitoring(this IServiceCollection services, IConfiguration configuration) - { - // Register health monitoring services - services.AddScoped(); - services.AddSingleton(); - - // Register security event monitoring services - services.AddSingleton(); - services.Configure(configuration.GetSection("SecurityMonitoring")); - - // Register health monitoring background service - services.Configure(configuration.GetSection("HealthMonitoring")); - services.AddHostedService(); - - // Register performance monitoring - services.AddSingleton(); - services.Configure(configuration.GetSection("PerformanceMonitoring")); - services.AddHostedService(provider => - provider.GetRequiredService() as PerformanceMonitoringService - ?? throw new InvalidOperationException("PerformanceMonitoringService not registered correctly")); - - // Register security event monitoring as hosted service - services.AddHostedService(provider => - provider.GetRequiredService() as ConduitLLM.Security.Services.SecurityEventMonitoringService - ?? throw new InvalidOperationException("SecurityEventMonitoringService not registered correctly")); - - // System resources health check removed per YAGNI principle - - // Register notification services - services.Configure(configuration.GetSection("HealthMonitoring:Notifications")); - services.Configure(configuration.GetSection("HealthMonitoring:Notifications:Webhook")); - services.Configure(configuration.GetSection("HealthMonitoring:Notifications:Email")); - services.Configure(configuration.GetSection("HealthMonitoring:Notifications:Slack")); - - // Register notification channels - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - // Register notification service - services.AddSingleton(); - - // Register batching service if enabled - var notificationOptions = configuration.GetSection("HealthMonitoring:Notifications").Get(); - if (notificationOptions?.EnableBatching == true) - { - services.AddSingleton(); - services.AddHostedService(provider => provider.GetRequiredService()); - } - - return services; - } - - /// - /// Adds advanced health monitoring checks (currently empty - removed unnecessary checks) - /// - public static IHealthChecksBuilder AddAdvancedHealthMonitoring( - this IHealthChecksBuilder healthChecksBuilder, - IConfiguration configuration) - { - // All advanced health checks have been removed per YAGNI principle - // Basic health checks are sufficient for monitoring service health - return healthChecksBuilder; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Extensions/SecurityOptionsExtensions.cs b/ConduitLLM.Http/Extensions/SecurityOptionsExtensions.cs deleted file mode 100644 index dc4cf76c7..000000000 --- a/ConduitLLM.Http/Extensions/SecurityOptionsExtensions.cs +++ /dev/null @@ -1,114 +0,0 @@ -using ConduitLLM.Http.Options; - -namespace ConduitLLM.Http.Extensions -{ - /// - /// Extension methods for configuring Core API security options - /// - public static class SecurityOptionsExtensions - { - /// - /// Configures Core API security options from configuration - /// - public static IServiceCollection ConfigureCoreApiSecurityOptions( - this IServiceCollection services, - IConfiguration configuration) - { - services.Configure(options => - { - // IP Filtering - options.IpFiltering.Enabled = configuration.GetValue("CONDUIT_CORE_IP_FILTERING_ENABLED") - ?? configuration.GetValue("CoreApi:Security:IpFiltering:Enabled", true); - - options.IpFiltering.Mode = configuration["CONDUIT_CORE_IP_FILTER_MODE"] - ?? configuration["CoreApi:Security:IpFiltering:Mode"] - ?? "permissive"; - - options.IpFiltering.AllowPrivateIps = configuration.GetValue("CONDUIT_CORE_IP_FILTER_ALLOW_PRIVATE") - ?? configuration.GetValue("CoreApi:Security:IpFiltering:AllowPrivateIps", true); - - // Parse whitelist - var whitelist = configuration["CONDUIT_CORE_IP_FILTER_WHITELIST"] - ?? configuration["CoreApi:Security:IpFiltering:Whitelist"]; - if (!string.IsNullOrEmpty(whitelist)) - { - options.IpFiltering.Whitelist = whitelist.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(ip => ip.Trim()) - .ToList(); - } - - // Parse blacklist - var blacklist = configuration["CONDUIT_CORE_IP_FILTER_BLACKLIST"] - ?? configuration["CoreApi:Security:IpFiltering:Blacklist"]; - if (!string.IsNullOrEmpty(blacklist)) - { - options.IpFiltering.Blacklist = blacklist.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(ip => ip.Trim()) - .ToList(); - } - - // Rate Limiting (IP-based) - options.RateLimiting.Enabled = configuration.GetValue("CONDUIT_CORE_RATE_LIMITING_ENABLED") - ?? configuration.GetValue("CoreApi:Security:RateLimiting:Enabled", true); - - options.RateLimiting.MaxRequests = configuration.GetValue("CONDUIT_CORE_RATE_LIMIT_MAX_REQUESTS") - ?? configuration.GetValue("CoreApi:Security:RateLimiting:MaxRequests", 1000); - - options.RateLimiting.WindowSeconds = configuration.GetValue("CONDUIT_CORE_RATE_LIMIT_WINDOW_SECONDS") - ?? configuration.GetValue("CoreApi:Security:RateLimiting:WindowSeconds", 60); - - // Parse excluded paths for rate limiting - var rateLimitExcluded = configuration["CONDUIT_CORE_RATE_LIMIT_EXCLUDED_PATHS"] - ?? configuration["CoreApi:Security:RateLimiting:ExcludedPaths"]; - if (!string.IsNullOrEmpty(rateLimitExcluded)) - { - options.RateLimiting.ExcludedPaths = rateLimitExcluded.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(path => path.Trim()) - .ToList(); - } - - // Failed Authentication Protection - options.FailedAuth.MaxAttempts = configuration.GetValue("CONDUIT_CORE_MAX_FAILED_AUTH_ATTEMPTS") - ?? configuration.GetValue("CoreApi:Security:FailedAuth:MaxAttempts", 10); - - options.FailedAuth.BanDurationMinutes = configuration.GetValue("CONDUIT_CORE_AUTH_BAN_DURATION_MINUTES") - ?? configuration.GetValue("CoreApi:Security:FailedAuth:BanDurationMinutes", 30); - - options.FailedAuth.TrackAcrossKeys = configuration.GetValue("CONDUIT_CORE_TRACK_FAILED_AUTH_ACROSS_KEYS") - ?? configuration.GetValue("CoreApi:Security:FailedAuth:TrackAcrossKeys", true); - - // Security Headers - options.Headers.XContentTypeOptions = configuration.GetValue("CONDUIT_CORE_SECURITY_HEADERS_CONTENT_TYPE") - ?? configuration.GetValue("CoreApi:Security:Headers:XContentTypeOptions", true); - - options.Headers.XXssProtection = configuration.GetValue("CONDUIT_CORE_SECURITY_HEADERS_XSS") - ?? configuration.GetValue("CoreApi:Security:Headers:XXssProtection", false); - - options.Headers.Hsts.Enabled = configuration.GetValue("CONDUIT_CORE_SECURITY_HEADERS_HSTS_ENABLED") - ?? configuration.GetValue("CoreApi:Security:Headers:Hsts:Enabled", true); - - options.Headers.Hsts.MaxAge = configuration.GetValue("CONDUIT_CORE_SECURITY_HEADERS_HSTS_MAX_AGE") - ?? configuration.GetValue("CoreApi:Security:Headers:Hsts:MaxAge", 31536000); - - // Distributed Tracking - options.UseDistributedTracking = configuration.GetValue("CONDUIT_SECURITY_USE_DISTRIBUTED_TRACKING") - ?? configuration.GetValue("Security:UseDistributedTracking", true); - - // Virtual Key Options - options.VirtualKey.EnforceRateLimits = configuration.GetValue("CONDUIT_CORE_ENFORCE_VKEY_RATE_LIMITS") - ?? configuration.GetValue("CoreApi:Security:VirtualKey:EnforceRateLimits", true); - - options.VirtualKey.EnforceBudgetLimits = configuration.GetValue("CONDUIT_CORE_ENFORCE_VKEY_BUDGETS") - ?? configuration.GetValue("CoreApi:Security:VirtualKey:EnforceBudgetLimits", true); - - options.VirtualKey.EnforceModelRestrictions = configuration.GetValue("CONDUIT_CORE_ENFORCE_VKEY_MODELS") - ?? configuration.GetValue("CoreApi:Security:VirtualKey:EnforceModelRestrictions", true); - - options.VirtualKey.ValidationCacheSeconds = configuration.GetValue("CONDUIT_CORE_VKEY_CACHE_SECONDS") - ?? configuration.GetValue("CoreApi:Security:VirtualKey:ValidationCacheSeconds", 60); - }); - - return services; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Extensions/ServiceCollectionExtensions.cs b/ConduitLLM.Http/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index 5ef466822..000000000 --- a/ConduitLLM.Http/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using ConduitLLM.Http.Interfaces; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using ConduitLLM.Http.Services; -using ConduitLLM.Http.Options; - -namespace ConduitLLM.Http.Extensions -{ - /// - /// Extension methods for service registration - /// - public static class ServiceCollectionExtensions - { - /// - /// Adds Core API security services to the service collection - /// - public static IServiceCollection AddCoreApiSecurity(this IServiceCollection services, IConfiguration configuration) - { - // Configure security options from environment variables - services.ConfigureCoreApiSecurityOptions(configuration); - - // Note: Distributed cache should be registered in Program.cs before calling this method - // to ensure proper Redis configuration for production environments - - // Register security service with factory to make distributed cache optional - services.AddSingleton(serviceProvider => - { - var options = serviceProvider.GetRequiredService>(); - var config = serviceProvider.GetRequiredService(); - var logger = serviceProvider.GetRequiredService>(); - var memoryCache = serviceProvider.GetRequiredService(); - - return new SecurityService(options, config, logger, memoryCache, serviceProvider); - }); - - // Register IP filter service as scoped since it depends on scoped repository - services.AddScoped(); - - return services; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Interfaces/IpFilterService.cs b/ConduitLLM.Http/Interfaces/IpFilterService.cs deleted file mode 100644 index ae2dbcb83..000000000 --- a/ConduitLLM.Http/Interfaces/IpFilterService.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Caching.Memory; -using ConduitLLM.Configuration.Constants; -using ConduitLLM.Configuration.Interfaces; - -namespace ConduitLLM.Http.Interfaces -{ - /// - /// Service for checking IP filter rules - /// - public interface IIpFilterService - { - /// - /// Checks if an IP address is allowed based on filter rules - /// - Task IsIpAllowedAsync(string ipAddress); - } - - /// - /// Implementation of IP filter service - /// - public class IpFilterService : IIpFilterService - { - private readonly IIpFilterRepository _repository; - private readonly IMemoryCache _cache; - private readonly ILogger _logger; - private const string CACHE_KEY = "ip_filters_enabled"; - private const int CACHE_DURATION_MINUTES = 5; - - /// - /// Initializes a new instance of the IpFilterService - /// - public IpFilterService( - IIpFilterRepository repository, - IMemoryCache cache, - ILogger logger) - { - _repository = repository; - _cache = cache; - _logger = logger; - } - - /// - public async Task IsIpAllowedAsync(string ipAddress) - { - try - { - // Get enabled filters from cache or database - var filters = await _cache.GetOrCreateAsync(CACHE_KEY, async entry => - { - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(CACHE_DURATION_MINUTES); - return await _repository.GetEnabledAsync(); - }); - - if (filters == null || filters.Count() == 0) - { - // No filters defined, allow all - return true; - } - - var filtersList = filters.ToList(); - var hasWhitelist = filtersList.Any(f => f.FilterType == IpFilterConstants.WHITELIST); - var hasBlacklist = filtersList.Any(f => f.FilterType == IpFilterConstants.BLACKLIST); - - // Check blacklist first - if IP is blacklisted, deny immediately - if (hasBlacklist) - { - foreach (var filter in filtersList.Where(f => f.FilterType == IpFilterConstants.BLACKLIST)) - { - if (IsIpInRange(ipAddress, filter.IpAddressOrCidr)) - { - _logger.LogWarning("IP {IpAddress} is blacklisted by rule {Rule}", - ipAddress, filter.IpAddressOrCidr); - return false; - } - } - } - - // If there's a whitelist, IP must be in it - if (hasWhitelist) - { - foreach (var filter in filtersList.Where(f => f.FilterType == IpFilterConstants.WHITELIST)) - { - if (IsIpInRange(ipAddress, filter.IpAddressOrCidr)) - { - _logger.LogDebug("IP {IpAddress} is whitelisted by rule {Rule}", - ipAddress, filter.IpAddressOrCidr); - return true; - } - } - - // Has whitelist but IP not in it - _logger.LogWarning("IP {IpAddress} is not in whitelist", ipAddress); - return false; - } - - // No whitelist and not blacklisted - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking IP filter for {IpAddress}", ipAddress); - // On error, default to allow to prevent blocking legitimate traffic - return true; - } - } - - private bool IsIpInRange(string ipAddress, string rule) - { - try - { - // Simple IP match - if (ipAddress == rule) - return true; - - // CIDR range check - if (rule.Contains('/')) - { - return IsIpInCidrRange(ipAddress, rule); - } - - return false; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking IP {IpAddress} against rule {Rule}", ipAddress, rule); - return false; - } - } - - private bool IsIpInCidrRange(string ipAddress, string cidrRange) - { - try - { - var parts = cidrRange.Split('/'); - if (parts.Length != 2) - return false; - - if (!IPAddress.TryParse(ipAddress, out var ip)) - return false; - - if (!IPAddress.TryParse(parts[0], out var baseAddress)) - return false; - - if (!int.TryParse(parts[1], out var prefixLength)) - return false; - - // Only support IPv4 for now - if (ip.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork || - baseAddress.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) - return false; - - var ipBytes = ip.GetAddressBytes(); - var baseBytes = baseAddress.GetAddressBytes(); - - // Calculate the mask - var maskBytes = new byte[4]; - for (int i = 0; i < 4; i++) - { - if (prefixLength >= 8) - { - maskBytes[i] = 0xFF; - prefixLength -= 8; - } - else if (prefixLength > 0) - { - maskBytes[i] = (byte)(0xFF << (8 - prefixLength)); - prefixLength = 0; - } - else - { - maskBytes[i] = 0x00; - } - } - - // Check if the IP is in the range - for (int i = 0; i < 4; i++) - { - if ((ipBytes[i] & maskBytes[i]) != (baseBytes[i] & maskBytes[i])) - return false; - } - - return true; - } - catch - { - return false; - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Middleware/BillingPolicyHandler.cs b/ConduitLLM.Http/Middleware/BillingPolicyHandler.cs deleted file mode 100644 index f988ba94e..000000000 --- a/ConduitLLM.Http/Middleware/BillingPolicyHandler.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System.Text.Json; -using ConduitLLM.Core.Models; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.Interfaces; - -namespace ConduitLLM.Http.Middleware -{ - /// - /// Static helper methods for billing policy decisions and audit logging. - /// Implements customer-friendly billing policy following Anthropic's approach. - /// - public static class BillingPolicyHandler - { - /// - /// Logs billing decisions for transparency and audit purposes. - /// Tracks when billing is skipped due to error responses or other policy reasons. - /// - public static Task LogBillingDecisionAsync(HttpContext context, IBillingAuditService billingAuditService, ILogger logger) - { - // Only log for API endpoints that would normally be tracked - if (!context.Request.Path.StartsWithSegments("/v1")) - return Task.CompletedTask; - - var path = context.Request.Path.Value?.ToLowerInvariant() ?? ""; - var isTrackableEndpoint = path.Contains("/completions") || - path.Contains("/embeddings") || - path.Contains("/images/generations") || - path.Contains("/audio/transcriptions") || - path.Contains("/audio/speech") || - path.Contains("/videos/generations"); - - if (!isTrackableEndpoint) - return Task.CompletedTask; - - var virtualKeyId = context.Items.TryGetValue("VirtualKeyId", out var keyId) ? keyId : "none"; - var statusCode = context.Response.StatusCode; - var requestId = context.TraceIdentifier; - - // Log reason for skipping billing - if (statusCode >= 400) - { - logger.LogDebug( - "Billing Policy: Skipping billing for error response - " + - "Status={StatusCode}, VirtualKey={VirtualKeyId}, Path={Path}, RequestId={RequestId}, " + - "Reason=ErrorResponse_NoChargePolicy", - statusCode, virtualKeyId, context.Request.Path, requestId); - - // Audit log error response skipped - var providerType = context.Items.TryGetValue("ProviderType", out var pt) ? pt?.ToString() : "unknown"; - billingAuditService.LogBillingEvent(new BillingAuditEvent - { - EventType = BillingAuditEventType.ErrorResponseSkipped, - VirtualKeyId = virtualKeyId is int vkId ? vkId : null, - RequestId = requestId, - RequestPath = context.Request.Path.ToString(), - HttpStatusCode = statusCode, - FailureReason = $"HTTP {statusCode} error response - no billing per policy", - ProviderType = providerType - }); - - // Increment metrics - UsageMetrics.BillingAuditEvents.WithLabels("ErrorResponseSkipped", providerType ?? "unknown").Inc(); - } - else if (!context.Items.ContainsKey("VirtualKeyId")) - { - logger.LogDebug( - "Billing Policy: Skipping billing - no virtual key found - " + - "Status={StatusCode}, Path={Path}, RequestId={RequestId}, " + - "Reason=NoVirtualKey", - statusCode, context.Request.Path, requestId); - - // Audit log no virtual key - billingAuditService.LogBillingEvent(new BillingAuditEvent - { - EventType = BillingAuditEventType.NoVirtualKey, - RequestId = requestId, - RequestPath = context.Request.Path.ToString(), - HttpStatusCode = statusCode, - FailureReason = "No virtual key found for request" - }); - - // Increment metrics - UsageMetrics.BillingAuditEvents.WithLabels("NoVirtualKey", "unknown").Inc(); - } - - return Task.CompletedTask; - } - - /// - /// Logs successful billing event with usage data. - /// - public static void LogSuccessfulBilling(HttpContext context, string model, Usage usage, decimal cost, - string providerType, IBillingAuditService billingAuditService, ILogger logger) - { - var virtualKeyId = (int)context.Items["VirtualKeyId"]!; - - billingAuditService.LogBillingEvent(new BillingAuditEvent - { - EventType = BillingAuditEventType.UsageTracked, - VirtualKeyId = virtualKeyId, - Model = model, - RequestId = context.TraceIdentifier, - UsageJson = JsonSerializer.Serialize(usage), - CalculatedCost = cost, - ProviderType = providerType, - RequestPath = context.Request.Path.ToString(), - HttpStatusCode = context.Response.StatusCode - }); - - // Increment metrics - UsageMetrics.BillingAuditEvents.WithLabels("UsageTracked", providerType ?? "unknown").Inc(); - UsageMetrics.BillingRevenue.WithLabels(model ?? "unknown", providerType ?? "unknown").Inc(Convert.ToDouble(cost)); - UsageMetrics.BillingCostDistribution.WithLabels(model ?? "unknown", providerType ?? "unknown").Observe(Convert.ToDouble(cost)); - } - - /// - /// Logs billing event for zero cost calculations. - /// - public static void LogZeroCostBilling(HttpContext context, string model, Usage usage, decimal cost, - string providerType, IBillingAuditService billingAuditService) - { - var virtualKeyId = (int)context.Items["VirtualKeyId"]!; - - billingAuditService.LogBillingEvent(new BillingAuditEvent - { - EventType = BillingAuditEventType.ZeroCostSkipped, - VirtualKeyId = virtualKeyId, - Model = model, - RequestId = context.TraceIdentifier, - UsageJson = JsonSerializer.Serialize(usage), - CalculatedCost = cost, - ProviderType = providerType, - RequestPath = context.Request.Path.ToString(), - HttpStatusCode = context.Response.StatusCode - }); - - // Increment metrics - UsageMetrics.BillingAuditEvents.WithLabels("ZeroCostSkipped", providerType ?? "unknown").Inc(); - UsageMetrics.ZeroCostEvents.WithLabels(model ?? "unknown", "calculated_zero").Inc(); - } - - /// - /// Logs billing event for missing usage data. - /// - public static void LogMissingUsageData(HttpContext context, IBillingAuditService billingAuditService) - { - var vkId = context.Items.ContainsKey("VirtualKeyId") ? (int?)context.Items["VirtualKeyId"] : null; - var providerType = context.Items.TryGetValue("ProviderType", out var pt) ? pt?.ToString() : "unknown"; - - billingAuditService.LogBillingEvent(new BillingAuditEvent - { - EventType = BillingAuditEventType.MissingUsageData, - VirtualKeyId = vkId, - RequestId = context.TraceIdentifier, - RequestPath = context.Request.Path.ToString(), - HttpStatusCode = context.Response.StatusCode, - ProviderType = providerType - }); - - // Increment metrics - UsageMetrics.BillingAuditEvents.WithLabels("MissingUsageData", providerType ?? "unknown").Inc(); - UsageMetrics.BillingRevenueLoss.WithLabels("MissingUsageData", "no_usage_in_response").Inc(); - } - - /// - /// Logs billing event for streaming usage. - /// - public static void LogStreamingBilling(HttpContext context, string model, Usage usage, decimal cost, - string providerType, bool isEstimated, IBillingAuditService billingAuditService, ILogger logger) - { - var virtualKeyId = (int)context.Items["VirtualKeyId"]!; - var eventType = isEstimated ? BillingAuditEventType.UsageEstimated : BillingAuditEventType.UsageTracked; - - billingAuditService.LogBillingEvent(new BillingAuditEvent - { - EventType = eventType, - VirtualKeyId = virtualKeyId, - Model = model, - RequestId = context.TraceIdentifier, - UsageJson = JsonSerializer.Serialize(usage), - CalculatedCost = cost, - ProviderType = providerType, - RequestPath = context.Request.Path.ToString(), - HttpStatusCode = context.Response.StatusCode, - IsEstimated = isEstimated, - FailureReason = isEstimated ? "Provider did not return usage data - usage was estimated conservatively" : null - }); - - // Increment metrics - UsageMetrics.BillingAuditEvents.WithLabels(eventType.ToString(), providerType ?? "unknown").Inc(); - - // Track estimated vs actual usage metrics - if (isEstimated) - { - logger.LogInformation("Successfully billed estimated usage for streaming response: Cost={Cost:C}", cost); - // Track that we recovered revenue through estimation - UsageMetrics.BillingRevenue.WithLabels(model ?? "unknown", providerType ?? "unknown_estimated").Inc(Convert.ToDouble(cost)); - } - UsageMetrics.BillingRevenue.WithLabels(model ?? "unknown", providerType ?? "unknown").Inc(Convert.ToDouble(cost)); - UsageMetrics.BillingCostDistribution.WithLabels(model ?? "unknown", providerType ?? "unknown").Observe(Convert.ToDouble(cost)); - } - - /// - /// Logs billing event for missing streaming usage data. - /// - public static void LogMissingStreamingUsage(HttpContext context, IBillingAuditService billingAuditService) - { - var vkId = context.Items.ContainsKey("VirtualKeyId") ? (int?)context.Items["VirtualKeyId"] : null; - var providerType = context.Items.TryGetValue("ProviderType", out var pt) ? pt?.ToString() : "unknown"; - - billingAuditService.LogBillingEvent(new BillingAuditEvent - { - EventType = BillingAuditEventType.StreamingUsageMissing, - VirtualKeyId = vkId, - RequestId = context.TraceIdentifier, - RequestPath = context.Request.Path.ToString(), - HttpStatusCode = context.Response.StatusCode, - FailureReason = "No StreamingUsage in HttpContext.Items - estimation service may not be configured", - ProviderType = providerType - }); - - // Increment metrics - UsageMetrics.BillingAuditEvents.WithLabels("StreamingUsageMissing", providerType ?? "unknown").Inc(); - UsageMetrics.BillingRevenueLoss.WithLabels("StreamingUsageMissing", "streaming_no_usage").Inc(); - } - - /// - /// Logs billing event for JSON parsing errors. - /// - public static void LogJsonParseError(HttpContext context, Exception ex, IBillingAuditService billingAuditService) - { - var virtualKeyId = context.Items.ContainsKey("VirtualKeyId") ? (int?)context.Items["VirtualKeyId"] : null; - var providerType = context.Items.TryGetValue("ProviderType", out var pt) ? pt?.ToString() : "unknown"; - - billingAuditService.LogBillingEvent(new BillingAuditEvent - { - EventType = BillingAuditEventType.JsonParseError, - VirtualKeyId = virtualKeyId, - RequestId = context.TraceIdentifier, - RequestPath = context.Request.Path.ToString(), - HttpStatusCode = context.Response.StatusCode, - FailureReason = ex.Message, - ProviderType = providerType - }); - - // Increment metrics - UsageMetrics.BillingAuditEvents.WithLabels("JsonParseError", providerType ?? "unknown").Inc(); - UsageMetrics.BillingRevenueLoss.WithLabels("JsonParseError", "parsing_failed").Inc(); - } - - /// - /// Logs billing event for unexpected errors. - /// - public static void LogUnexpectedError(HttpContext context, Exception ex, IBillingAuditService billingAuditService) - { - var virtualKeyId = context.Items.ContainsKey("VirtualKeyId") ? (int?)context.Items["VirtualKeyId"] : null; - var providerType = context.Items.TryGetValue("ProviderType", out var pt) ? pt?.ToString() : "unknown"; - - billingAuditService.LogBillingEvent(new BillingAuditEvent - { - EventType = BillingAuditEventType.UnexpectedError, - VirtualKeyId = virtualKeyId, - RequestId = context.TraceIdentifier, - RequestPath = context.Request.Path.ToString(), - HttpStatusCode = context.Response.StatusCode, - FailureReason = ex.Message, - ProviderType = providerType - }); - - // Increment metrics - UsageMetrics.BillingAuditEvents.WithLabels("UnexpectedError", providerType ?? "unknown").Inc(); - UsageMetrics.BillingRevenueLoss.WithLabels("UnexpectedError", "exception").Inc(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Middleware/SecurityMiddleware.cs b/ConduitLLM.Http/Middleware/SecurityMiddleware.cs deleted file mode 100644 index ced4fb950..000000000 --- a/ConduitLLM.Http/Middleware/SecurityMiddleware.cs +++ /dev/null @@ -1,128 +0,0 @@ -using ConduitLLM.Http.Services; -using ConduitLLM.Security.Interfaces; - -namespace ConduitLLM.Http.Middleware -{ - /// - /// Unified security middleware for Core API that handles IP filtering, rate limiting, and ban checks - /// - public class SecurityMiddleware - { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the SecurityMiddleware - /// - public SecurityMiddleware(RequestDelegate next, ILogger logger) - { - _next = next; - _logger = logger; - } - - /// - /// Processes the HTTP request through security checks - /// - public async Task InvokeAsync(HttpContext context, ISecurityService securityService, ISecurityEventMonitoringService? securityEventMonitoring = null) - { - var clientIp = GetClientIpAddress(context); - var endpoint = context.Request.Path.Value ?? ""; - - // Pass along any authentication failure info from VirtualKeyAuthenticationMiddleware - if (context.Response.StatusCode == 401) - { - // Authentication already failed, don't continue - return; - } - - var result = await securityService.IsRequestAllowedAsync(context); - - if (!result.IsAllowed) - { - _logger.LogWarning("Request blocked: {Reason} for path {Path} from IP {IP}", - result.Reason, - context.Request.Path, - context.Connection.RemoteIpAddress); - - // Record security events based on the reason - if (securityEventMonitoring != null) - { - var virtualKey = context.Items["AttemptedKey"] as string ?? ""; - - if (result.Reason.Contains("rate limit", StringComparison.OrdinalIgnoreCase)) - { - var limitType = result.Headers.ContainsKey("X-RateLimit-Scope") - ? result.Headers["X-RateLimit-Scope"] - : "general"; - securityEventMonitoring.RecordRateLimitViolation(clientIp, virtualKey, endpoint, limitType); - } - else if (result.Reason.Contains("banned", StringComparison.OrdinalIgnoreCase)) - { - // IP ban is already recorded by SecurityService - } - else - { - securityEventMonitoring.RecordSuspiciousActivity(clientIp, "Access Denied", result.Reason); - } - } - - context.Response.StatusCode = result.StatusCode ?? 403; - - // Add any response headers - foreach (var header in result.Headers) - { - context.Response.Headers.Append(header.Key, header.Value); - } - - // Return JSON error response - await context.Response.WriteAsJsonAsync(new - { - error = result.Reason, - code = result.StatusCode - }); - return; - } - - await _next(context); - } - - private string GetClientIpAddress(HttpContext context) - { - // Check X-Forwarded-For header first (for reverse proxies) - var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); - if (!string.IsNullOrEmpty(forwardedFor)) - { - // Take the first IP in the chain - var ip = forwardedFor.Split(',').First().Trim(); - if (System.Net.IPAddress.TryParse(ip, out _)) - { - return ip; - } - } - - // Check X-Real-IP header - var realIp = context.Request.Headers["X-Real-IP"].FirstOrDefault(); - if (!string.IsNullOrEmpty(realIp) && System.Net.IPAddress.TryParse(realIp, out _)) - { - return realIp; - } - - // Fall back to direct connection IP - return context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; - } - } - - /// - /// Extension methods for SecurityMiddleware - /// - public static class SecurityMiddlewareExtensions - { - /// - /// Adds the security middleware to the pipeline - /// - public static IApplicationBuilder UseCoreApiSecurity(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Middleware/UsageExtractor.cs b/ConduitLLM.Http/Middleware/UsageExtractor.cs deleted file mode 100644 index 71882b2e5..000000000 --- a/ConduitLLM.Http/Middleware/UsageExtractor.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Text.Json; -using ConduitLLM.Core.Models; - -namespace ConduitLLM.Http.Middleware -{ - /// - /// Static helper methods for extracting usage data from LLM API responses. - /// Supports multiple provider formats including OpenAI and Anthropic. - /// - public static class UsageExtractor - { - /// - /// Extracts usage data from a JSON response element. - /// - /// The usage JSON element from the response - /// Logger for error reporting - /// Extracted usage data or null if extraction fails - public static Usage? ExtractUsage(JsonElement usageElement, ILogger logger) - { - try - { - var usage = new Usage(); - - // Standard OpenAI fields - if (usageElement.TryGetProperty("prompt_tokens", out var promptTokens)) - usage.PromptTokens = promptTokens.GetInt32(); - - if (usageElement.TryGetProperty("completion_tokens", out var completionTokens)) - usage.CompletionTokens = completionTokens.GetInt32(); - - if (usageElement.TryGetProperty("total_tokens", out var totalTokens)) - usage.TotalTokens = totalTokens.GetInt32(); - - // Anthropic format (uses input_tokens/output_tokens) - // Note: These will override OpenAI fields if both exist - if (usageElement.TryGetProperty("input_tokens", out var inputTokens)) - usage.PromptTokens = inputTokens.GetInt32(); - - if (usageElement.TryGetProperty("output_tokens", out var outputTokens)) - usage.CompletionTokens = outputTokens.GetInt32(); - - // Anthropic cached tokens - if (usageElement.TryGetProperty("cache_creation_input_tokens", out var cacheWriteTokens)) - usage.CachedWriteTokens = cacheWriteTokens.GetInt32(); - - if (usageElement.TryGetProperty("cache_read_input_tokens", out var cacheReadTokens)) - usage.CachedInputTokens = cacheReadTokens.GetInt32(); - - // Image generation - if (usageElement.TryGetProperty("images", out var imageCount)) - usage.ImageCount = imageCount.GetInt32(); - - // Validate we have at least some usage data - if (usage.PromptTokens == null && - usage.CompletionTokens == null && - usage.ImageCount == null) - { - return null; - } - - return usage; - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to extract usage data from response"); - return null; - } - } - - /// - /// Determines the request type from the API path. - /// - /// The request path - /// The type of request (chat, completion, embedding, etc.) - public static string DetermineRequestType(PathString path) - { - var pathValue = path.Value?.ToLowerInvariant() ?? ""; - - if (pathValue.Contains("/chat/completions")) - return "chat"; - if (pathValue.Contains("/completions")) - return "completion"; - if (pathValue.Contains("/embeddings")) - return "embedding"; - if (pathValue.Contains("/images/generations")) - return "image"; - if (pathValue.Contains("/audio/transcriptions")) - return "transcription"; - if (pathValue.Contains("/audio/speech")) - return "tts"; - if (pathValue.Contains("/videos/generations")) - return "video"; - - return "other"; - } - - /// - /// Calculates the response time from the request start time stored in HttpContext. - /// - /// The HTTP context - /// Response time in milliseconds - public static double GetResponseTime(HttpContext context) - { - if (context.Items.TryGetValue("RequestStartTime", out var startTimeObj) && - startTimeObj is DateTime startTime) - { - return (DateTime.UtcNow - startTime).TotalMilliseconds; - } - - return 0; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Middleware/UsageTrackingMiddleware.cs b/ConduitLLM.Http/Middleware/UsageTrackingMiddleware.cs deleted file mode 100644 index dc64416ae..000000000 --- a/ConduitLLM.Http/Middleware/UsageTrackingMiddleware.cs +++ /dev/null @@ -1,393 +0,0 @@ -using System.Text.Json; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Configuration.Interfaces; -using ConduitLLM.Configuration.DTOs; -using Prometheus; -using IVirtualKeyService = ConduitLLM.Core.Interfaces.IVirtualKeyService; - -namespace ConduitLLM.Http.Middleware -{ - /// - /// Middleware that tracks LLM usage by intercepting OpenAI-compatible responses. - /// Extracts usage data from responses and updates virtual key spending. - /// - public class UsageTrackingMiddleware - { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public UsageTrackingMiddleware( - RequestDelegate next, - ILogger logger) - { - _next = next ?? throw new ArgumentNullException(nameof(next)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Processes HTTP requests to track LLM usage and billing. - /// - /// Billing Policy: - /// - Only successful responses (2xx) are billed to customers - /// - Client errors (4xx) are NOT billed - protects customers from malformed requests - /// - Server errors (5xx) are NOT billed - our infrastructure failures shouldn't cost customers - /// - Rate limiting (429) is NOT billed - capacity management shouldn't penalize customers - /// - /// This follows Anthropic's customer-friendly approach rather than OpenAI's partial billing model. - /// The policy ensures customers only pay for successfully processed requests that deliver value. - /// - public async Task InvokeAsync( - HttpContext context, - ICostCalculationService costCalculationService, - IBatchSpendUpdateService batchSpendService, - IRequestLogService requestLogService, - IVirtualKeyService virtualKeyService, - IBillingAuditService billingAuditService) - { - // Skip if not an API endpoint or no virtual key - if (!ShouldTrackUsage(context)) - { - // Log billing decision for error responses if this is a tracked endpoint type - await LogBillingDecisionAsync(context, billingAuditService); - await _next(context); - return; - } - - // For non-streaming responses, intercept the response body - var originalBodyStream = context.Response.Body; - - try - { - using var responseBody = new MemoryStream(); - context.Response.Body = responseBody; - - await _next(context); - - // After the controller has run, check if this is a streaming response - // by checking the Content-Type that was set by the controller - if (context.Response.ContentType?.Contains("text/event-stream") == true) - { - _logger.LogDebug("Detected streaming response, skipping JSON parsing"); - // For streaming, just copy the stream directly without parsing - responseBody.Seek(0, SeekOrigin.Begin); - await responseBody.CopyToAsync(originalBodyStream); - await TrackStreamingUsageAsync(context, costCalculationService, batchSpendService, - requestLogService, virtualKeyService, billingAuditService); - return; - } - - // Process non-streaming response - await ProcessResponseAsync( - context, - responseBody, - costCalculationService, - batchSpendService, - requestLogService, - virtualKeyService, - billingAuditService); - - // Copy the response body back to the original stream - responseBody.Seek(0, SeekOrigin.Begin); - await responseBody.CopyToAsync(originalBodyStream); - } - finally - { - context.Response.Body = originalBodyStream; - } - } - - private bool ShouldTrackUsage(HttpContext context) - { - // Check if this is an API request - if (!context.Request.Path.StartsWithSegments("/v1")) - return false; - - // Check if we have a virtual key in the context - if (!context.Items.ContainsKey("VirtualKeyId")) - return false; - - // Only track successful responses - core billing policy enforcement - if (context.Response.StatusCode >= 400) - return false; - - // Only track completion endpoints - var path = context.Request.Path.Value?.ToLowerInvariant() ?? ""; - return path.Contains("/completions") || - path.Contains("/embeddings") || - path.Contains("/images/generations") || - path.Contains("/audio/transcriptions") || - path.Contains("/audio/speech") || - path.Contains("/videos/generations"); - } - - private async Task ProcessResponseAsync( - HttpContext context, - MemoryStream responseBody, - ICostCalculationService costCalculationService, - IBatchSpendUpdateService batchSpendService, - IRequestLogService requestLogService, - IVirtualKeyService virtualKeyService, - IBillingAuditService billingAuditService) - { - var endpointType = UsageExtractor.DetermineRequestType(context.Request.Path); - using var extractionTimer = UsageMetrics.UsageExtractionTime.WithLabels(endpointType).NewTimer(); - - try - { - responseBody.Seek(0, SeekOrigin.Begin); - - // Parse the response JSON - using var jsonDocument = await JsonDocument.ParseAsync(responseBody); - var root = jsonDocument.RootElement; - - // Extract usage data if present - if (!root.TryGetProperty("usage", out var usageElement)) - { - _logger.LogDebug("No usage data found in response for {Path}", context.Request.Path); - LogMissingUsageData(context, billingAuditService); - return; - } - - // Extract model name - if (!root.TryGetProperty("model", out var modelElement)) - { - _logger.LogWarning("No model found in response for {Path}", context.Request.Path); - return; - } - - var model = modelElement.GetString(); - if (string.IsNullOrEmpty(model)) - { - _logger.LogWarning("Empty model name in response for {Path}", context.Request.Path); - return; - } - - // Build Usage object - var usage = UsageExtractor.ExtractUsage(usageElement, _logger); - if (usage == null) - { - _logger.LogWarning("Failed to extract usage data for {Path}", context.Request.Path); - return; - } - - // Get virtual key ID - var virtualKeyId = (int)context.Items["VirtualKeyId"]!; - - // Get provider type for metrics - var providerType = context.Items.TryGetValue("ProviderType", out var providerTypeObj) - ? providerTypeObj?.ToString() ?? "unknown" - : "unknown"; - - // Calculate cost - var cost = await costCalculationService.CalculateCostAsync(model, usage); - - if (cost <= 0) - { - _logger.LogDebug("Zero cost calculated for {Model} with usage {Usage}", model, JsonSerializer.Serialize(usage)); - UsageMetrics.UsageTrackingFailures.WithLabels("zero_cost", endpointType).Inc(); - LogZeroCostBilling(context, model, usage, cost, providerType, billingAuditService); - return; - } - - // Update metrics - UsageMetrics.UsageTrackingRequests.WithLabels(endpointType, "success").Inc(); - - if (usage.PromptTokens.HasValue) - UsageMetrics.UsageTrackingTokens.WithLabels(model, providerType, "prompt").Inc(usage.PromptTokens.Value); - - if (usage.CompletionTokens.HasValue) - UsageMetrics.UsageTrackingTokens.WithLabels(model, providerType, "completion").Inc(usage.CompletionTokens.Value); - - UsageMetrics.UsageTrackingCosts.WithLabels(model, providerType, endpointType).Inc(Convert.ToDouble(cost)); - - // Update spend using batch service - await SpendUpdateHelper.UpdateSpendAsync(virtualKeyId, cost, batchSpendService, virtualKeyService, _logger); - - // Log the request - await LogRequestAsync(context, virtualKeyId, model, usage, cost, requestLogService); - - // Audit log successful billing - LogSuccessfulBilling(context, model, usage, cost, providerType, billingAuditService); - } - catch (JsonException ex) - { - _logger.LogError(ex, "Failed to parse response JSON for usage tracking"); - UsageMetrics.UsageTrackingFailures.WithLabels("json_parse_error", endpointType).Inc(); - LogJsonParseError(context, ex, billingAuditService); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unexpected error in usage tracking"); - UsageMetrics.UsageTrackingFailures.WithLabels("unexpected_error", endpointType).Inc(); - LogUnexpectedError(context, ex, billingAuditService); - } - } - - private async Task TrackStreamingUsageAsync( - HttpContext context, - ICostCalculationService costCalculationService, - IBatchSpendUpdateService batchSpendService, - IRequestLogService requestLogService, - IVirtualKeyService virtualKeyService, - IBillingAuditService billingAuditService) - { - var endpointType = UsageExtractor.DetermineRequestType(context.Request.Path); - - // Check if usage was estimated - var isEstimated = context.Items.TryGetValue("UsageIsEstimated", out var estimatedObj) && - estimatedObj is bool estimated && estimated; - - // For streaming responses, we need to rely on the SSE writer - // to have stored the usage data in HttpContext.Items - if (!context.Items.TryGetValue("StreamingUsage", out var usageObj) || - usageObj is not Usage usage) - { - _logger.LogDebug("No streaming usage data found for {Path}", context.Request.Path); - UsageMetrics.UsageTrackingFailures.WithLabels("no_streaming_usage", endpointType).Inc(); - LogMissingStreamingUsage(context, billingAuditService); - return; - } - - if (!context.Items.TryGetValue("StreamingModel", out var modelObj) || - modelObj is not string model) - { - _logger.LogWarning("No streaming model found for {Path}", context.Request.Path); - UsageMetrics.UsageTrackingFailures.WithLabels("no_streaming_model", endpointType).Inc(); - return; - } - - var virtualKeyId = (int)context.Items["VirtualKeyId"]!; - - // Get provider type for metrics - var providerType = context.Items.TryGetValue("ProviderType", out var providerTypeObj) - ? providerTypeObj?.ToString() ?? "unknown" - : "unknown"; - - // Calculate cost and track - var cost = await costCalculationService.CalculateCostAsync(model, usage); - if (cost > 0) - { - // Update metrics - UsageMetrics.UsageTrackingRequests.WithLabels(endpointType + "_stream", "success").Inc(); - - if (usage.PromptTokens.HasValue) - UsageMetrics.UsageTrackingTokens.WithLabels(model, providerType, "prompt").Inc(usage.PromptTokens.Value); - - if (usage.CompletionTokens.HasValue) - UsageMetrics.UsageTrackingTokens.WithLabels(model, providerType, "completion").Inc(usage.CompletionTokens.Value); - - UsageMetrics.UsageTrackingCosts.WithLabels(model, providerType, endpointType + "_stream").Inc(Convert.ToDouble(cost)); - - await SpendUpdateHelper.UpdateSpendAsync(virtualKeyId, cost, batchSpendService, virtualKeyService, _logger); - await LogRequestAsync(context, virtualKeyId, model, usage, cost, requestLogService); - - LogStreamingBilling(context, model, usage, cost, providerType, isEstimated, billingAuditService); - } - else - { - UsageMetrics.UsageTrackingFailures.WithLabels("zero_cost_streaming", endpointType).Inc(); - LogZeroCostBilling(context, model, usage, cost, providerType, billingAuditService); - UsageMetrics.ZeroCostEvents.WithLabels(model ?? "unknown", "streaming_zero").Inc(); - } - } - - private async Task LogRequestAsync( - HttpContext context, - int virtualKeyId, - string model, - Usage usage, - decimal cost, - IRequestLogService requestLogService) - { - try - { - var requestType = UsageExtractor.DetermineRequestType(context.Request.Path); - - var logRequest = new LogRequestDto - { - VirtualKeyId = virtualKeyId, - ModelName = model, - RequestType = requestType, - InputTokens = usage.PromptTokens ?? 0, - OutputTokens = usage.CompletionTokens ?? 0, - Cost = cost, - ResponseTimeMs = UsageExtractor.GetResponseTime(context), - UserId = context.User?.Identity?.Name, - ClientIp = context.Connection.RemoteIpAddress?.ToString(), - RequestPath = context.Request.Path.ToString(), - StatusCode = context.Response.StatusCode - }; - - await requestLogService.LogRequestAsync(logRequest); - - _logger.LogInformation( - "Tracked usage for VirtualKey {VirtualKeyId}: Model={Model}, PromptTokens={PromptTokens}, CompletionTokens={CompletionTokens}, Cost={Cost:C}", - virtualKeyId, model, usage.PromptTokens, usage.CompletionTokens, cost); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to log request for VirtualKey {VirtualKeyId}", virtualKeyId); - // Don't throw - logging failure shouldn't break the request - } - } - - #region Billing Audit Logging - - private async Task LogBillingDecisionAsync(HttpContext context, IBillingAuditService billingAuditService) - { - await BillingPolicyHandler.LogBillingDecisionAsync(context, billingAuditService, _logger); - } - - private void LogSuccessfulBilling(HttpContext context, string model, Usage usage, decimal cost, - string providerType, IBillingAuditService billingAuditService) - { - BillingPolicyHandler.LogSuccessfulBilling(context, model, usage, cost, providerType, billingAuditService, _logger); - } - - private void LogZeroCostBilling(HttpContext context, string model, Usage usage, decimal cost, - string providerType, IBillingAuditService billingAuditService) - { - BillingPolicyHandler.LogZeroCostBilling(context, model, usage, cost, providerType, billingAuditService); - } - - private void LogMissingUsageData(HttpContext context, IBillingAuditService billingAuditService) - { - BillingPolicyHandler.LogMissingUsageData(context, billingAuditService); - } - - private void LogStreamingBilling(HttpContext context, string model, Usage usage, decimal cost, - string providerType, bool isEstimated, IBillingAuditService billingAuditService) - { - BillingPolicyHandler.LogStreamingBilling(context, model, usage, cost, providerType, isEstimated, billingAuditService, _logger); - } - - private void LogMissingStreamingUsage(HttpContext context, IBillingAuditService billingAuditService) - { - BillingPolicyHandler.LogMissingStreamingUsage(context, billingAuditService); - } - - private void LogJsonParseError(HttpContext context, Exception ex, IBillingAuditService billingAuditService) - { - BillingPolicyHandler.LogJsonParseError(context, ex, billingAuditService); - } - - private void LogUnexpectedError(HttpContext context, Exception ex, IBillingAuditService billingAuditService) - { - BillingPolicyHandler.LogUnexpectedError(context, ex, billingAuditService); - } - - #endregion - } - - /// - /// Extension methods for registering the usage tracking middleware - /// - public static class UsageTrackingMiddlewareExtensions - { - public static IApplicationBuilder UseUsageTracking(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Models/SystemNotification.cs b/ConduitLLM.Http/Models/SystemNotification.cs deleted file mode 100644 index b227ac2cb..000000000 --- a/ConduitLLM.Http/Models/SystemNotification.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace ConduitLLM.Http.Models -{ - // This file has been moved to ConduitLLM.Configuration.DTOs.SignalR.SystemNotifications - // All types are now available via type forwarding - // TODO: Remove this file in the next major version - - // Type forwarding for backward compatibility - [Obsolete("Use ConduitLLM.Configuration.DTOs.SignalR.SystemNotification instead")] - public abstract class SystemNotification : ConduitLLM.Configuration.DTOs.SignalR.SystemNotification { } - - [Obsolete("Use ConduitLLM.Configuration.DTOs.SignalR.ProviderHealthNotification instead")] - public class ProviderHealthNotification : ConduitLLM.Configuration.DTOs.SignalR.ProviderHealthNotification { } - - [Obsolete("Use ConduitLLM.Configuration.DTOs.SignalR.RateLimitNotification instead")] - public class RateLimitNotification : ConduitLLM.Configuration.DTOs.SignalR.RateLimitNotification { } - - [Obsolete("Use ConduitLLM.Configuration.DTOs.SignalR.SystemAnnouncementNotification instead")] - public class SystemAnnouncementNotification : ConduitLLM.Configuration.DTOs.SignalR.SystemAnnouncementNotification { } - - [Obsolete("Use ConduitLLM.Configuration.DTOs.SignalR.ServiceDegradationNotification instead")] - public class ServiceDegradationNotification : ConduitLLM.Configuration.DTOs.SignalR.ServiceDegradationNotification { } - - [Obsolete("Use ConduitLLM.Configuration.DTOs.SignalR.ServiceRestorationNotification instead")] - public class ServiceRestorationNotification : ConduitLLM.Configuration.DTOs.SignalR.ServiceRestorationNotification { } - - [Obsolete("Use ConduitLLM.Configuration.DTOs.SignalR.ModelMappingNotification instead")] - public class ModelMappingNotification : ConduitLLM.Configuration.DTOs.SignalR.ModelMappingNotification { } - - [Obsolete("Use ConduitLLM.Configuration.DTOs.SignalR.ModelCapabilitiesNotification instead")] - public class ModelCapabilitiesNotification : ConduitLLM.Configuration.DTOs.SignalR.ModelCapabilitiesNotification { } - - [Obsolete("Use ConduitLLM.Configuration.DTOs.SignalR.ModelAvailabilityNotification instead")] - public class ModelAvailabilityNotification : ConduitLLM.Configuration.DTOs.SignalR.ModelAvailabilityNotification { } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Program.CoreServices.cs b/ConduitLLM.Http/Program.CoreServices.cs deleted file mode 100644 index 3d248c28c..000000000 --- a/ConduitLLM.Http/Program.CoreServices.cs +++ /dev/null @@ -1,525 +0,0 @@ -using ConduitLLM.Configuration.Extensions; -using ConduitLLM.Configuration.Interfaces; -using ConduitLLM.Core; -using ConduitLLM.Core.Extensions; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Services; -using ConduitLLM.Http.Extensions; -using ConduitLLM.Http.Security; -using ConduitLLM.Http.Services; -using ConduitLLM.Providers.Extensions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Distributed; -using Polly; -using Polly.Extensions.Http; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using MassTransit; - -public partial class Program -{ - public static void ConfigureCoreServices(WebApplicationBuilder builder) - { - // Rate Limiter registration - builder.Services.AddRateLimiter(options => - { - options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; - options.AddPolicy("VirtualKeyPolicy", context => - { - // Use the actual partition provider from the policy instance - var policy = context.RequestServices.GetRequiredService(); - return policy.GetPartition(context); - }); - }); - builder.Services.AddScoped(); - - // Model costs tracking service - builder.Services.AddScoped(); - - // Ephemeral key service for direct browser-to-API authentication (used for all direct access including SignalR) - builder.Services.AddScoped(); - - builder.Services.AddScoped(); - - // Parameter validation service for minimal, provider-agnostic validation - builder.Services.AddScoped(); - - // Virtual key service (Configuration layer - used by RealtimeUsageTracker) - builder.Services.AddScoped(); - - // Billing audit service for comprehensive billing event tracking - builder.Services.AddSingleton(); - builder.Services.AddHostedService(provider => - provider.GetRequiredService() as ConduitLLM.Configuration.Services.BillingAuditService - ?? throw new InvalidOperationException("BillingAuditService must implement IHostedService")); - - // Provider error tracking service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - builder.Services.AddMemoryCache(); - - // Add cache infrastructure with distributed statistics collection - builder.Services.AddCacheInfrastructure(builder.Configuration); - - // Configure OpenTelemetry with metrics - builder.Services.AddOpenTelemetry() - .WithMetrics(meterProviderBuilder => - { - meterProviderBuilder - .SetResourceBuilder(OpenTelemetry.Resources.ResourceBuilder.CreateDefault() - .AddService(serviceName: "ConduitLLM.Http", serviceVersion: "1.0.0")) - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation() - .AddProcessInstrumentation() - .AddMeter("ConduitLLM.SignalR") // Add SignalR metrics - .AddPrometheusExporter(); - }); - - // Register monitoring services - builder.Services.AddSingleton(); - builder.Services.AddHostedService(provider => - provider.GetRequiredService()); - - // Register new SignalR reliability services - builder.Services.AddSingleton(); - builder.Services.AddHostedService(provider => - (ConduitLLM.Http.Services.SignalRAcknowledgmentService)provider.GetRequiredService()); - - builder.Services.AddSingleton(); - builder.Services.AddHostedService(provider => - (ConduitLLM.Http.Services.SignalRMessageQueueService)provider.GetRequiredService()); - - builder.Services.AddSingleton(); - builder.Services.AddHostedService(provider => - (ConduitLLM.Http.Services.SignalRConnectionMonitor)provider.GetRequiredService()); - - builder.Services.AddSingleton(); - builder.Services.AddHostedService(provider => - (ConduitLLM.Http.Services.SignalRMessageBatcher)provider.GetRequiredService()); - - // Register SignalR OpenTelemetry metrics - builder.Services.AddSingleton(); - builder.Services.AddHostedService(); - - builder.Services.AddHostedService(); - builder.Services.AddHostedService(); - - // 2. Register DbContext Factory (using connection string from environment variables) - var connectionStringManager = new ConduitLLM.Core.Data.ConnectionStringManager(); - // Pass "CoreAPI" to get Core API-specific connection pool settings - var (dbProvider, dbConnectionString) = connectionStringManager.GetProviderAndConnectionString("CoreAPI", msg => Console.WriteLine(msg)); - - // Log the connection pool settings for verification - if (dbProvider == "postgres" && dbConnectionString.Contains("MaxPoolSize")) - { - Console.WriteLine($"[Conduit] Core API database connection pool configured:"); - var match = System.Text.RegularExpressions.Regex.Match(dbConnectionString, @"MinPoolSize=(\d+);MaxPoolSize=(\d+)"); - if (match.Success) - { - Console.WriteLine($"[Conduit] Min Pool Size: {match.Groups[1].Value}"); - Console.WriteLine($"[Conduit] Max Pool Size: {match.Groups[2].Value}"); - } - } - - // Only PostgreSQL is supported - if (dbProvider != "postgres") - { - throw new InvalidOperationException($"Only PostgreSQL is supported. Invalid provider: {dbProvider}"); - } - - builder.Services.AddDbContextFactory(options => - { - options.UseNpgsql(dbConnectionString); - // Suppress PendingModelChangesWarning in production - if (builder.Environment.IsProduction()) - { - options.ConfigureWarnings(warnings => warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)); - } - }); - - // Also add scoped registration from factory for services that need direct injection - // Note: This creates contexts from the factory on demand - builder.Services.AddScoped(provider => - { - var factory = provider.GetService>(); - if (factory == null) - { - throw new InvalidOperationException("IDbContextFactory is not registered"); - } - return factory.CreateDbContext(); - }); - - // Authentication and authorization are configured later with policies - - // Add Core API Security services - builder.Services.AddCoreApiSecurity(builder.Configuration); - - // Add all the service registrations BEFORE calling builder.Build() - // Register HttpClientFactory - REQUIRED for LLMClientFactory - builder.Services.AddHttpClient(); - - // Add standard LLM provider HTTP clients with timeout/retry policies - builder.Services.AddLLMProviderHttpClients(); - - // Add video generation HTTP clients without timeout for long-running operations - builder.Services.AddVideoGenerationHttpClients(); - - // Register operation timeout provider for operation-aware timeout policies - builder.Services.AddSingleton(); - - // Add dependencies needed for the Conduit service - // Use DatabaseAwareLLMClientFactory to get provider credentials from database - builder.Services.AddScoped(); - - // Add Provider Registry - single source of truth for provider metadata - builder.Services.AddSingleton(); - Console.WriteLine("[ConduitLLM.Http] Provider Registry registered - centralized provider metadata management enabled"); - - // Add performance metrics service - builder.Services.AddSingleton(); - - // 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(); - - // Register all repositories using the extension method - builder.Services.AddRepositories(); - - // Register services - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - - // Register System Notification Service - builder.Services.AddSingleton(); - - // Register Model Metadata Service - builder.Services.AddSingleton(); - - // Register TaskHub Service for ITaskHub interface - builder.Services.AddSingleton(); - - // Register Batch Operation Services - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddSingleton(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - - // Register Webhook Delivery Service - builder.Services.AddSingleton(); - - // Register Spend Notification Service - builder.Services.AddSingleton(); - builder.Services.AddHostedService(sp => - (ConduitLLM.Http.Services.SpendNotificationService)sp.GetRequiredService()); - - // Register Webhook Delivery Notification Service - builder.Services.AddSingleton(); - builder.Services.AddHostedService(sp => - (ConduitLLM.Http.Services.WebhookDeliveryNotificationService)sp.GetRequiredService()); - - // Model Capability Service is registered via ServiceCollectionExtensions - - // Provider Discovery Service is only used in Admin API for dynamic model discovery - // Core API relies on configured model mappings only - - // Register Video Generation Service with explicit dependencies - builder.Services.AddScoped(sp => - { - var clientFactory = sp.GetRequiredService(); - var capabilityService = sp.GetRequiredService(); - var costService = sp.GetRequiredService(); - var virtualKeyService = sp.GetRequiredService(); - var mediaStorage = sp.GetRequiredService(); - var taskService = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - var modelMappingService = sp.GetRequiredService(); - var publishEndpoint = sp.GetService(); // Optional - var taskRegistry = sp.GetService(); // Optional - - return new VideoGenerationService( - clientFactory, - capabilityService, - costService, - virtualKeyService, - mediaStorage, - taskService, - logger, - modelMappingService, - publishEndpoint, - taskRegistry); - }); - - // Configure Video Generation Retry Settings - builder.Services.Configure(options => - { - options.MaxRetries = builder.Configuration.GetValue("VideoGeneration:MaxRetries", 3); - options.BaseDelaySeconds = builder.Configuration.GetValue("VideoGeneration:BaseDelaySeconds", 30); - options.MaxDelaySeconds = builder.Configuration.GetValue("VideoGeneration:MaxDelaySeconds", 3600); - options.EnableRetries = builder.Configuration.GetValue("VideoGeneration:EnableRetries", true); - options.RetryCheckIntervalSeconds = builder.Configuration.GetValue("VideoGeneration:RetryCheckIntervalSeconds", 30); - }); - - // Register HTTP client for image downloads with retry policies - builder.Services.AddHttpClient("ImageDownload", client => - { - client.Timeout = TimeSpan.FromSeconds(60); // Timeout for large images - client.DefaultRequestHeaders.Add("User-Agent", "Conduit-LLM-ImageDownloader/1.0"); - client.DefaultRequestHeaders.Add("Accept", "image/*"); - }) - .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler - { - PooledConnectionLifetime = TimeSpan.FromMinutes(5), - PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), - MaxConnectionsPerServer = 20, - EnableMultipleHttp2Connections = true, - MaxResponseHeadersLength = 64 * 1024, - ResponseDrainTimeout = TimeSpan.FromSeconds(10), - ConnectTimeout = TimeSpan.FromSeconds(10), - AutomaticDecompression = System.Net.DecompressionMethods.All, // Handle gzip/deflate - AllowAutoRedirect = true, // Handle redirects automatically - MaxAutomaticRedirections = 5 // Limit redirect chains - }) - .AddPolicyHandler(GetImageDownloadRetryPolicy()) - .AddPolicyHandler(Policy.TimeoutAsync(TimeSpan.FromSeconds(120))); // Overall timeout including retries - - // Register HTTP client for video downloads with retry policies - builder.Services.AddHttpClient("VideoDownload", client => - { - client.Timeout = TimeSpan.FromMinutes(10); // Much longer timeout for large videos - client.DefaultRequestHeaders.Add("User-Agent", "Conduit-LLM-VideoDownloader/1.0"); - client.DefaultRequestHeaders.Add("Accept", "video/*"); - }) - .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler - { - PooledConnectionLifetime = TimeSpan.FromMinutes(10), - PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5), - MaxConnectionsPerServer = 10, // Fewer connections for large transfers - EnableMultipleHttp2Connections = true, - MaxResponseHeadersLength = 64 * 1024, - ResponseDrainTimeout = TimeSpan.FromSeconds(30), - ConnectTimeout = TimeSpan.FromSeconds(30), - AutomaticDecompression = System.Net.DecompressionMethods.All, - AllowAutoRedirect = true, - MaxAutomaticRedirections = 5 - }) - .AddPolicyHandler(GetVideoDownloadRetryPolicy()) - .AddPolicyHandler(Policy.TimeoutAsync(TimeSpan.FromMinutes(15))); // Overall timeout including retries - - // Register Webhook Notification Service with optimized configuration for high throughput - builder.Services.AddTransient(); - builder.Services.AddHttpClient( - "WebhookClient", - client => - { - client.Timeout = TimeSpan.FromSeconds(10); // Reduced from 30s for better scalability - client.DefaultRequestHeaders.Add("User-Agent", "Conduit-LLM/1.0"); - client.DefaultRequestHeaders.ConnectionClose = false; // Keep-alive for connection reuse - }) - .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler - { - PooledConnectionLifetime = TimeSpan.FromMinutes(5), // Refresh connections every 5 minutes - PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), // Close idle connections after 2 minutes - MaxConnectionsPerServer = 100, // Support 1000+ webhooks/min (17/sec avg, 100 concurrent) - EnableMultipleHttp2Connections = true, // Allow multiple HTTP/2 connections - MaxResponseHeadersLength = 64 * 1024, // 64KB for headers - ResponseDrainTimeout = TimeSpan.FromSeconds(5), // Drain response within 5 seconds - ConnectTimeout = TimeSpan.FromSeconds(5), // Connection timeout - KeepAlivePingTimeout = TimeSpan.FromSeconds(20), // HTTP/2 keep-alive ping timeout - KeepAlivePingDelay = TimeSpan.FromSeconds(30) // HTTP/2 keep-alive ping delay - }) - .AddPolicyHandler(GetWebhookRetryPolicy()) - .AddPolicyHandler(GetWebhookCircuitBreakerPolicy()) - .AddHttpMessageHandler(); - - // Register Webhook Circuit Breaker for preventing repeated failures - builder.Services.AddMemoryCache(); // Ensure memory cache is available - builder.Services.AddSingleton(sp => - { - var cache = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - - // Configure circuit breaker: open after 5 failures, stay open for 5 minutes - return new ConduitLLM.Core.Services.WebhookCircuitBreaker( - cache, - logger, - failureThreshold: 5, - openDuration: TimeSpan.FromMinutes(5), - counterResetDuration: TimeSpan.FromMinutes(15)); - }); - - // Register provider model list service - // OBSOLETE: External model discovery is no longer used. - // The ProviderModelsController now returns models from the local database. - // builder.Services.AddScoped(); - - // Model discovery providers have been migrated to sister classes - - // Configure HttpClient for discovery providers - builder.Services.AddHttpClient("DiscoveryProviders", client => - { - client.Timeout = TimeSpan.FromSeconds(30); - client.DefaultRequestHeaders.Add("User-Agent", "Conduit-LLM/1.0"); - }); - - - // Register async task service - // Register cancellable task registry - builder.Services.AddSingleton(); - - // Always use hybrid database+cache task management - // This provides consistency across all deployments and proper event publishing - builder.Services.AddScoped(sp => - { - var repository = sp.GetRequiredService(); - var cache = sp.GetRequiredService(); - var publishEndpoint = sp.GetService(); // Optional - var logger = sp.GetRequiredService>(); - - return publishEndpoint != null - ? new ConduitLLM.Core.Services.HybridAsyncTaskService(repository, cache, publishEndpoint, logger) - : new ConduitLLM.Core.Services.HybridAsyncTaskService(repository, cache, logger); - }); - - // Register Conduit service - builder.Services.AddScoped(); - - // Register File Retrieval Service - builder.Services.AddScoped(); - - // Register Audio services - builder.Services.AddConduitAudioServices(builder.Configuration); - - // Register Batch Cache Invalidation service - builder.Services.AddBatchCacheInvalidation(builder.Configuration); - - // Register Discovery Cache service for model discovery endpoint caching - builder.Services.AddDiscoveryCache(builder.Configuration); - - // Register Discovery Cache warming as a hosted service (runs on startup) - builder.Services.AddHostedService(); - - // 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( - builder.Configuration.GetSection("ConduitLLM:ImageGenerationRetry")); - - // Add background services for monitoring and cleanup (skip in test environment to prevent endless loops) - if (builder.Environment.EnvironmentName != "Test") - { - // Add database-based background service for image generation - // REMOVED: ImageGenerationDatabaseBackgroundService - Events are now processed by ImageGenerationOrchestrator consumer - - // DISABLED: VideoGenerationBackgroundService causes duplicate event publishing - // The VideoGenerationService already publishes VideoGenerationRequested events directly - // builder.Services.AddHostedService(); - - // Add background service for image generation metrics cleanup - // ImageGenerationMetricsCleanupService removed - metrics handled differently now - } - - Console.WriteLine("[Conduit] Image generation configured with database-first architecture"); - Console.WriteLine("[Conduit] Image generation supports multi-instance deployment with lease-based task processing"); - Console.WriteLine("[Conduit] Image generation performance tracking and optimization enabled"); - } - - // Polly retry policy for image downloads with exponential backoff - static IAsyncPolicy GetImageDownloadRetryPolicy() - { - return HttpPolicyExtensions - .HandleTransientHttpError() // Handles HttpRequestException and 5XX, 408 status codes - .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests) - .WaitAndRetryAsync( - 3, // Retry up to 3 times - retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // Exponential backoff: 2, 4, 8 seconds - onRetry: (outcome, timespan, retryCount, context) => - { - // Log retry attempts (logger will be injected via DI in actual use) - var logger = context.Values.FirstOrDefault() as ILogger; - logger?.LogWarning("Image download retry {RetryCount} after {Delay}ms", retryCount, timespan.TotalMilliseconds); - }); - } - - // Polly retry policy for video downloads with longer exponential backoff - static IAsyncPolicy GetVideoDownloadRetryPolicy() - { - return HttpPolicyExtensions - .HandleTransientHttpError() - .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests) - .WaitAndRetryAsync( - 3, // Retry up to 3 times - retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)), // Longer backoff: 3, 9, 27 seconds - onRetry: (outcome, timespan, retryCount, context) => - { - var logger = context.Values.FirstOrDefault() as ILogger; - logger?.LogWarning("Video download retry {RetryCount} after {Delay}s", retryCount, timespan.TotalSeconds); - }); - } - - // Polly retry policy for webhook delivery - static IAsyncPolicy GetWebhookRetryPolicy() - { - return HttpPolicyExtensions - .HandleTransientHttpError() - .OrResult(msg => !msg.IsSuccessStatusCode && msg.StatusCode != System.Net.HttpStatusCode.BadRequest) - .WaitAndRetryAsync( - 3, - retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // Exponential backoff: 2s, 4s, 8s - onRetry: (outcome, timespan, retryCount, context) => - { - // Log retry attempts to console (logger not available in static context) - Console.WriteLine($"[Webhook Retry] Attempt {retryCount} after {timespan.TotalMilliseconds}ms. Status: {outcome.Result?.StatusCode.ToString() ?? "N/A"}"); - }); - } - - // Polly circuit breaker policy for webhook delivery - static IAsyncPolicy GetWebhookCircuitBreakerPolicy() - { - return HttpPolicyExtensions - .HandleTransientHttpError() - .CircuitBreakerAsync( - handledEventsAllowedBeforeBreaking: 5, - durationOfBreak: TimeSpan.FromMinutes(1), - onBreak: (result, duration) => - { - // Circuit breaker opened - this will be logged by the WebhookCircuitBreaker service - Console.WriteLine($"[Webhook Circuit Breaker] Opened for {duration.TotalSeconds} seconds"); - }, - onReset: () => - { - // Circuit breaker closed - Console.WriteLine("[Webhook Circuit Breaker] Reset"); - }); - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Program.Endpoints.cs b/ConduitLLM.Http/Program.Endpoints.cs deleted file mode 100644 index 2ee7faf26..000000000 --- a/ConduitLLM.Http/Program.Endpoints.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Text.Json; - -public partial class Program -{ - public static void ConfigureEndpoints(WebApplication app) - { - // Get JsonSerializerOptions from DI - var jsonSerializerOptions = app.Services.GetRequiredService(); - - // Map SignalR hubs for real-time updates - - // Customer-facing hubs require virtual key authentication - app.MapHub("/hubs/video-generation") - .RequireAuthorization(); - Console.WriteLine("[Conduit API] SignalR VideoGenerationHub registered at /hubs/video-generation (requires authentication)"); - - // Public video generation hub using task-scoped tokens (no virtual key required) - app.MapHub("/hubs/public/video-generation"); - Console.WriteLine("[Conduit API] SignalR PublicVideoGenerationHub registered at /hubs/public/video-generation (token-based auth)"); - - app.MapHub("/hubs/image-generation") - .RequireAuthorization(); - Console.WriteLine("[Conduit API] SignalR ImageGenerationHub registered at /hubs/image-generation (requires authentication)"); - - app.MapHub("/hubs/tasks") - .RequireAuthorization(); - Console.WriteLine("[Conduit API] SignalR TaskHub registered at /hubs/tasks (requires authentication)"); - - app.MapHub("/hubs/notifications") - .RequireAuthorization(); - Console.WriteLine("[Conduit API] SignalR SystemNotificationHub registered at /hubs/notifications (requires authentication)"); - - app.MapHub("/hubs/spend") - .RequireAuthorization(); - Console.WriteLine("[Conduit API] SignalR SpendNotificationHub registered at /hubs/spend (requires authentication)"); - - app.MapHub("/hubs/webhooks") - .RequireAuthorization(); - Console.WriteLine("[Conduit API] SignalR WebhookDeliveryHub registered at /hubs/webhooks (requires authentication)"); - - - // Admin-only hub for metrics dashboard - app.MapHub("/hubs/metrics") - .RequireAuthorization("AdminOnly"); - Console.WriteLine("[Conduit API] SignalR MetricsHub registered at /hubs/metrics (requires admin authentication)"); - - // Admin-only hub for health monitoring - app.MapHub("/hubs/health-monitoring") - .RequireAuthorization("AdminOnly"); - Console.WriteLine("[Conduit API] SignalR HealthMonitoringHub registered at /hubs/health-monitoring (requires admin authentication)"); - - // Admin-only hub for security monitoring - app.MapHub("/hubs/security-monitoring") - .RequireAuthorization("AdminOnly"); - Console.WriteLine("[Conduit API] SignalR SecurityMonitoringHub registered at /hubs/security-monitoring (requires admin authentication)"); - - // Virtual key management hub for real-time key management updates - app.MapHub("/hubs/virtual-key-management") - .RequireAuthorization(); - Console.WriteLine("[Conduit API] SignalR VirtualKeyManagementHub registered at /hubs/virtual-key-management (requires authentication)"); - - // Usage analytics hub for real-time analytics and monitoring - app.MapHub("/hubs/usage-analytics") - .RequireAuthorization(); - Console.WriteLine("[Conduit API] SignalR UsageAnalyticsHub registered at /hubs/usage-analytics (requires authentication)"); - - // Enhanced video generation hub with acknowledgment support - app.MapHub("/hubs/enhanced-video-generation") - .RequireAuthorization(); - Console.WriteLine("[Conduit API] SignalR EnhancedVideoGenerationHub registered at /hubs/enhanced-video-generation (requires authentication)"); - - // Map health check endpoints - app.MapHealthChecks("/health", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions - { - // Exclude monitoring and performance checks from basic health endpoint - Predicate = check => !check.Tags.Contains("monitoring") && !check.Tags.Contains("performance") - }); - app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions - { - Predicate = check => check.Tags.Contains("live") - }); - app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions - { - Predicate = check => check.Tags.Contains("ready") || check.Tags.Count == 0 - }); - - // Map Prometheus metrics endpoint for scraping - app.UseOpenTelemetryPrometheusScrapingEndpoint("/metrics"); - Console.WriteLine("[Conduit API] Prometheus metrics endpoint registered at /metrics"); - - Console.WriteLine("[Conduit API] All API endpoints are now handled by controllers."); - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Program.Media.cs b/ConduitLLM.Http/Program.Media.cs deleted file mode 100644 index 9cfcc33af..000000000 --- a/ConduitLLM.Http/Program.Media.cs +++ /dev/null @@ -1,41 +0,0 @@ -using ConduitLLM.Core.Extensions; -using ConduitLLM.Http.Services; -using ConduitLLM.Configuration.Options; - -public partial class Program -{ - public static void ConfigureMediaServices(WebApplicationBuilder builder) - { - Console.WriteLine("[Conduit] ConfigureMediaServices - Using shared media configuration"); - - // Use the shared media services configuration from ConduitLLM.Core - builder.Services.AddMediaServices(builder.Configuration); - - // Configure media lifecycle options - builder.Services.Configure( - builder.Configuration.GetSection(MediaLifecycleOptions.SectionName)); - - // Add distributed media scheduler service for lifecycle management - builder.Services.AddHostedService(); - - // Legacy: Media maintenance background service (will be removed after migration) - // builder.Services.AddHostedService(); - - Console.WriteLine("[Conduit] Media lifecycle management configured:"); - - var mediaOptions = builder.Configuration - .GetSection(MediaLifecycleOptions.SectionName) - .Get() ?? new MediaLifecycleOptions(); - - Console.WriteLine($" - Scheduler Mode: {mediaOptions.SchedulerMode}"); - Console.WriteLine($" - Dry Run Mode: {mediaOptions.DryRunMode}"); - Console.WriteLine($" - Schedule Interval: {mediaOptions.ScheduleIntervalMinutes} minutes"); - Console.WriteLine($" - Max Batch Size: {mediaOptions.MaxBatchSize} items"); - Console.WriteLine($" - Monthly Delete Budget: {mediaOptions.MonthlyDeleteBudget:N0} operations"); - - if (mediaOptions.TestVirtualKeyGroups.Any()) - { - Console.WriteLine($" - Test Groups: {string.Join(", ", mediaOptions.TestVirtualKeyGroups)}"); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Program.Messaging.cs b/ConduitLLM.Http/Program.Messaging.cs deleted file mode 100644 index ee2a1c27b..000000000 --- a/ConduitLLM.Http/Program.Messaging.cs +++ /dev/null @@ -1,371 +0,0 @@ -using ConduitLLM.Core.Services; - -using MassTransit; - -public partial class Program -{ - public static void ConfigureMessagingServices(WebApplicationBuilder builder) - { - // Configure RabbitMQ settings - var rabbitMqConfig = builder.Configuration.GetSection("ConduitLLM:RabbitMQ").Get() - ?? new ConduitLLM.Configuration.RabbitMqConfiguration(); - - // Check if RabbitMQ is configured - var useRabbitMq = !string.IsNullOrEmpty(rabbitMqConfig.Host) && rabbitMqConfig.Host != "localhost"; - - // Register MassTransit event bus - builder.Services.AddMassTransit(x => - { - // Add event consumers for Core API - x.AddConsumer(); - x.AddConsumer(); - x.AddConsumer(); - - // Add spend notification consumer - x.AddConsumer(); - - - // Add image generation consumers - x.AddConsumer(); - x.AddConsumer(); - x.AddConsumer(); - x.AddConsumer(); - - // Add video generation consumers - x.AddConsumer(); - x.AddConsumer(); - x.AddConsumer(); - x.AddConsumer(); - x.AddConsumer(); - - // Add Admin API event consumers for cache invalidation - x.AddConsumer(); - x.AddConsumer(); - - // Add async task cache invalidation handler - x.AddConsumer(); - x.AddConsumer(); - - // Add navigation state event consumers for real-time updates - x.AddConsumer(); - // Provider health consumer removed - - // Add settings refresh consumers for runtime configuration updates - x.AddConsumer(); - x.AddConsumer(); - - // Add media lifecycle handler for tracking generated media - x.AddConsumer(); - - // Add video generation started handler for real-time notifications - x.AddConsumer(); - - // Add webhook delivery consumer for scalable webhook processing - x.AddConsumer(); - - // Add batch spend flush handler for admin operations and integration testing - x.AddConsumer(); - - // Add media lifecycle consumers for retention policy management - x.AddConsumer(); - x.AddConsumer(); - x.AddConsumer(); - x.AddConsumer(); - - if (useRabbitMq) - { - x.UsingRabbitMq((context, cfg) => - { - // Configure RabbitMQ connection - cfg.Host(new Uri($"rabbitmq://{rabbitMqConfig.Host}:{rabbitMqConfig.Port}{rabbitMqConfig.VHost}"), h => - { - h.Username(rabbitMqConfig.Username); - h.Password(rabbitMqConfig.Password); - h.Heartbeat(TimeSpan.FromSeconds(rabbitMqConfig.RequestedHeartbeat)); - - // High throughput settings - h.PublisherConfirmation = rabbitMqConfig.PublisherConfirmation; - - // Advanced connection settings - h.RequestedChannelMax(rabbitMqConfig.ChannelMax); - h.RequestedConnectionTimeout(TimeSpan.FromSeconds(30)); - }); - - // Configure prefetch count for consumer concurrency - cfg.PrefetchCount = rabbitMqConfig.PrefetchCount; - - // Configure retry policy for reliability - cfg.UseMessageRetry(r => r.Incremental(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2))); - - // Configure delayed redelivery for failed messages - cfg.UseDelayedRedelivery(r => r.Intervals(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30))); - - // Configure webhook delivery endpoint optimized for 1000+ webhooks/minute - cfg.ReceiveEndpoint("webhook-delivery", e => - { - // Configure for high throughput - balanced for memory usage - e.PrefetchCount = 100; // Reduced from 200 to prevent memory overload - e.ConcurrentMessageLimit = 75; // Balanced concurrency - - // Use quorum queue for better reliability - e.SetQuorumQueue(); - e.SetQueueArgument("x-delivery-limit", 10); // Max redelivery attempts - e.SetQueueArgument("x-max-length", 50000); // Queue size limit - e.SetQueueArgument("x-overflow", "reject-publish"); // Reject new messages when full - - // Configure retry with exponential backoff - e.UseMessageRetry(r => r.Exponential(3, - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(30), - TimeSpan.FromSeconds(2))); - - // Circuit breaker to prevent cascading failures - e.UseCircuitBreaker(cb => - { - cb.TrackingPeriod = TimeSpan.FromMinutes(1); - cb.TripThreshold = 15; // 15% failure rate - cb.ActiveThreshold = 10; // Minimum attempts before evaluating - cb.ResetInterval = TimeSpan.FromMinutes(5); - }); - - // Rate limiting to prevent consumer overload - e.UseRateLimit(100, TimeSpan.FromSeconds(1)); // 100 messages per second - - // Prevents duplicate sends during retries - // Note: UseInMemoryOutbox is now configured at the bus level - - e.ConfigureConsumer(context, c => - { - c.UseConcurrentMessageLimit(75); - }); - }); - - // Configure video generation endpoint for high throughput - cfg.ReceiveEndpoint("video-generation-events", e => - { - e.PrefetchCount = rabbitMqConfig.PrefetchCount; - e.ConcurrentMessageLimit = rabbitMqConfig.ConcurrentMessageLimit; - - // Enable consume topology to properly bind consumers to the queue - // This ensures VideoGenerationRequested events are routed to this endpoint - e.ConfigureConsumeTopology = true; - e.SetQuorumQueue(); - // Note: Removed x-single-active-consumer as it conflicts with partitioned processing - // Ordering is maintained through partition keys in the event messages - - // Retry policy for transient failures - e.UseMessageRetry(r => r.Incremental(3, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5))); - - // Circuit breaker - e.UseCircuitBreaker(cb => - { - cb.TrackingPeriod = TimeSpan.FromMinutes(2); - cb.TripThreshold = 20; // 20% failure rate - cb.ActiveThreshold = 5; - cb.ResetInterval = TimeSpan.FromMinutes(10); - }); - - e.ConfigureConsumer(context); - e.ConfigureConsumer(context); - }); - - // Configure image generation endpoint - cfg.ReceiveEndpoint("image-generation-events", e => - { - e.PrefetchCount = rabbitMqConfig.PrefetchCount; - e.ConcurrentMessageLimit = rabbitMqConfig.ConcurrentMessageLimit; - - e.SetQuorumQueue(); - e.SetQueueArgument("x-single-active-consumer", true); - - e.UseMessageRetry(r => r.Incremental(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3))); - - e.UseCircuitBreaker(cb => - { - cb.TrackingPeriod = TimeSpan.FromMinutes(1); - cb.TripThreshold = 15; - cb.ActiveThreshold = 5; - cb.ResetInterval = TimeSpan.FromMinutes(5); - }); - - e.ConfigureConsumer(context); - }); - - // Configure spend update endpoint with strict ordering - cfg.ReceiveEndpoint("spend-update-events", e => - { - e.PrefetchCount = 10; // Lower prefetch for ordered processing - e.ConcurrentMessageLimit = 1; // Sequential processing per partition - - e.SetQuorumQueue(); - e.SetQueueArgument("x-single-active-consumer", true); - e.SetQueueArgument("x-max-length", 10000); - - e.UseMessageRetry(r => r.Immediate(3)); - - e.ConfigureConsumer(context); - }); - - // Configure media retention policy evaluation endpoint - cfg.ReceiveEndpoint("media-retention-checks", e => - { - e.PrefetchCount = 5; // Low prefetch for controlled processing - e.ConcurrentMessageLimit = 3; // Limited concurrency - - e.SetQuorumQueue(); - e.SetQueueArgument("x-max-length", 1000); - - e.UseMessageRetry(r => r.Incremental(3, - TimeSpan.FromSeconds(2), - TimeSpan.FromSeconds(5))); - - e.UseCircuitBreaker(cb => - { - cb.TrackingPeriod = TimeSpan.FromMinutes(2); - cb.TripThreshold = 15; // 15% failure rate - cb.ActiveThreshold = 5; - cb.ResetInterval = TimeSpan.FromMinutes(10); - }); - - e.ConfigureConsumer(context); - }); - - // Configure media cleanup batch processing endpoint - cfg.ReceiveEndpoint("media-cleanup-batches", e => - { - e.PrefetchCount = 10; - e.ConcurrentMessageLimit = 5; - - e.SetQuorumQueue(); - - e.UseMessageRetry(r => r.Immediate(2)); - - e.ConfigureConsumer(context); - }); - - // Configure R2 batch operations endpoint with strict rate limiting for free tier - cfg.ReceiveEndpoint("r2-batch-operations", e => - { - e.PrefetchCount = 2; // Very low prefetch for R2 free tier - e.ConcurrentMessageLimit = 1; // Sequential processing to avoid rate limits - - e.SetQuorumQueue(); - e.SetQueueArgument("x-single-active-consumer", true); // Single consumer for rate control - e.SetQueueArgument("x-max-length", 500); // Limit queue size - - // Exponential backoff for R2 rate limit handling - e.UseMessageRetry(r => r.Exponential(10, - TimeSpan.FromSeconds(5), - TimeSpan.FromMinutes(10), - TimeSpan.FromSeconds(2))); - - // Rate limiting to stay within R2 free tier limits - e.UseRateLimit(5, TimeSpan.FromSeconds(1)); // Max 5 operations per second - - // Circuit breaker for R2 service issues - e.UseCircuitBreaker(cb => - { - cb.TrackingPeriod = TimeSpan.FromMinutes(5); - cb.TripThreshold = 20; // 20% failure rate - cb.ActiveThreshold = 10; - cb.ResetInterval = TimeSpan.FromMinutes(15); - }); - - e.ConfigureConsumer(context); - }); - - // Configure media cleanup schedule endpoint - cfg.ReceiveEndpoint("media-cleanup-schedule", e => - { - e.PrefetchCount = 1; - e.ConcurrentMessageLimit = 1; // Single processing for schedules - - e.SetQuorumQueue(); - e.SetQueueArgument("x-single-active-consumer", true); - - e.UseMessageRetry(r => r.Immediate(2)); - - e.ConfigureConsumer(context); - }); - - // Configure dead letter exchange at the endpoint level - // Dead letter queues are configured per endpoint above - - // Configure remaining endpoints with automatic topology - cfg.ConfigureEndpoints(context); - }); - - Console.WriteLine($"[Conduit] Event bus configured with RabbitMQ transport (multi-instance mode) - Host: {rabbitMqConfig.Host}:{rabbitMqConfig.Port}"); - Console.WriteLine("[Conduit] Event-driven architecture ENABLED - Services will publish events for:"); - Console.WriteLine(" - Virtual Key updates (cache invalidation across instances)"); - Console.WriteLine(" - Spend updates (ordered processing with race condition prevention)"); - Console.WriteLine(" - Provider credential changes (automatic capability refresh)"); - Console.WriteLine(" - Model capability discovery (shared across all instances)"); - Console.WriteLine(" - Model mapping changes (real-time WebUI updates via SignalR)"); - Console.WriteLine(" - Provider health changes (real-time WebUI updates via SignalR)"); - Console.WriteLine(" - Global settings changes (system-wide configuration updates)"); - Console.WriteLine(" - IP filter changes (security policy updates)"); - Console.WriteLine(" - Model cost changes (pricing updates)"); - Console.WriteLine(" - Video generation tasks (partitioned processing per virtual key)"); - Console.WriteLine(" - Image generation tasks (partitioned processing per virtual key)"); - Console.WriteLine(" - Media lifecycle management (retention policies and cleanup)"); - Console.WriteLine(" - R2 batch operations (rate-limited for free tier compliance)"); - } - else - { - x.UsingInMemory((context, cfg) => - { - // NOTE: Using in-memory transport for single-instance deployments - // Configure RabbitMQ environment variables for multi-instance production - - // Configure retry policy for reliability - cfg.UseMessageRetry(r => r.Incremental(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2))); - - // Configure delayed redelivery for failed messages - cfg.UseDelayedRedelivery(r => r.Intervals(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30))); - - // Configure webhook delivery endpoint with high throughput settings - cfg.ReceiveEndpoint("webhook-delivery", e => - { - // Configure retry with shorter intervals for webhook scenarios - e.UseMessageRetry(r => r.Exponential(3, - TimeSpan.FromSeconds(1), // Faster initial retry - TimeSpan.FromSeconds(30), // Max backoff - TimeSpan.FromSeconds(2))); - - // Prevents duplicate sends during retries - // Note: UseInMemoryOutbox is now configured at the bus level - - e.ConfigureConsumer(context, c => - { - // Configure consumer concurrency for in-memory - c.UseConcurrentMessageLimit(50); // Lower for single instance - }); - }); - - // Configure endpoints with automatic topology - cfg.ConfigureEndpoints(context); - }); - - Console.WriteLine("[Conduit] Event bus configured with in-memory transport (single-instance mode)"); - Console.WriteLine("[Conduit] Event-driven architecture ENABLED - Services will publish events locally"); - Console.WriteLine("[Conduit] WARNING: For production multi-instance deployments, configure RabbitMQ:"); - Console.WriteLine(" - Set CONDUITLLM__RABBITMQ__HOST to your RabbitMQ host"); - Console.WriteLine(" - Set CONDUITLLM__RABBITMQ__USERNAME and CONDUITLLM__RABBITMQ__PASSWORD"); - Console.WriteLine(" - This enables cache consistency and ordered processing across instances"); - } - }); - - // Register batch webhook publisher for high-throughput webhook delivery - if (useRabbitMq) - { - builder.Services.AddBatchWebhookPublisher(options => - { - options.MaxBatchSize = 100; - options.MaxBatchDelay = TimeSpan.FromMilliseconds(100); - options.ConcurrentPublishers = 3; - }); - Console.WriteLine("[Conduit] Batch webhook publisher configured for high-throughput delivery"); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Program.Monitoring.cs b/ConduitLLM.Http/Program.Monitoring.cs deleted file mode 100644 index e5ceb0148..000000000 --- a/ConduitLLM.Http/Program.Monitoring.cs +++ /dev/null @@ -1,135 +0,0 @@ -using ConduitLLM.Configuration.Data; -using ConduitLLM.Http.Extensions; - -public partial class Program -{ - public static void ConfigureMonitoringServices(WebApplicationBuilder builder) - { - // Add Controller support - builder.Services.AddControllers(); - - // Add Swagger/OpenAPI support - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo - { - Title = "Conduit Core API", - Version = "v1", - Description = "OpenAI-compatible API for multi-provider LLM access" - }); - - // Add API Key authentication - c.AddSecurityDefinition("ApiKey", new Microsoft.OpenApi.Models.OpenApiSecurityScheme - { - Description = "Virtual Key authentication using Authorization header", - Name = "Authorization", - In = Microsoft.OpenApi.Models.ParameterLocation.Header, - Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey, - Scheme = "Bearer" - }); - - c.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement - { - { - new Microsoft.OpenApi.Models.OpenApiSecurityScheme - { - Reference = new Microsoft.OpenApi.Models.OpenApiReference - { - Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, - Id = "ApiKey" - } - }, - Array.Empty() - } - }); - - // Include XML comments if available - var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; - var xmlPath = System.IO.Path.Combine(AppContext.BaseDirectory, xmlFile); - if (System.IO.File.Exists(xmlPath)) - { - c.IncludeXmlComments(xmlPath); - } - }); - - // Get Redis and RabbitMQ configuration for health checks - var redisUrl = Environment.GetEnvironmentVariable("REDIS_URL"); - var redisConnectionString = Environment.GetEnvironmentVariable("CONDUIT_REDIS_CONNECTION_STRING"); - - if (!string.IsNullOrEmpty(redisUrl)) - { - try - { - redisConnectionString = ConduitLLM.Configuration.Utilities.RedisUrlParser.ParseRedisUrl(redisUrl); - } - catch - { - // Failed to parse REDIS_URL, will use legacy connection string if available - } - } - - var connectionStringManager = new ConduitLLM.Core.Data.ConnectionStringManager(); - var (dbProvider, dbConnectionString) = connectionStringManager.GetProviderAndConnectionString("CoreAPI", msg => Console.WriteLine(msg)); - - var rabbitMqConfig = builder.Configuration.GetSection("ConduitLLM:RabbitMQ").Get() - ?? new ConduitLLM.Configuration.RabbitMqConfiguration(); - - // Check if RabbitMQ is configured - var useRabbitMq = !string.IsNullOrEmpty(rabbitMqConfig.Host) && rabbitMqConfig.Host != "localhost"; - - // Add standardized health checks (skip in test environment to avoid conflicts) - if (builder.Environment.EnvironmentName != "Test") - { - // Add basic health checks - var healthChecksBuilder = builder.Services.AddHealthChecks(); - - // Add comprehensive RabbitMQ health check if RabbitMQ is configured - if (useRabbitMq) - { - healthChecksBuilder.AddCheck( - "rabbitmq_comprehensive", - failureStatus: Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Unhealthy, - tags: new[] { "messaging", "rabbitmq", "performance", "monitoring" }); - } - - // Add Redis health check if Redis is configured - if (!string.IsNullOrEmpty(redisConnectionString)) - { - var redisConnStr = redisConnectionString; // Capture for closure - healthChecksBuilder.AddTypeActivatedCheck( - "redis", - failureStatus: Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Unhealthy, - tags: new[] { "cache", "redis", "billing" }, - args: new object[] { redisConnStr }); - - // Add circuit breaker health check - healthChecksBuilder.AddCheck( - "redis_circuit_breaker", - failureStatus: Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Unhealthy, - tags: new[] { "circuit_breaker", "redis", "resilience" }); - } - - // Audio health checks removed per YAGNI principle - - // Add advanced health monitoring checks (includes SignalR and HTTP connection pool checks) - healthChecksBuilder.AddAdvancedHealthMonitoring(builder.Configuration); - } - - // Add health monitoring services - builder.Services.AddHealthMonitoring(builder.Configuration); - - // Add database migration services - builder.Services.AddDatabaseMigration(); - - // Add connection pool warmer for better startup performance - builder.Services.AddHostedService(serviceProvider => - { - var logger = serviceProvider.GetRequiredService>(); - return new ConduitLLM.Core.Services.ConnectionPoolWarmer(serviceProvider, logger, "CoreAPI"); - }); - - // Add cache statistics registration service - builder.Services.AddHostedService(); - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Program.SignalR.cs b/ConduitLLM.Http/Program.SignalR.cs deleted file mode 100644 index 83ef6a32f..000000000 --- a/ConduitLLM.Http/Program.SignalR.cs +++ /dev/null @@ -1,165 +0,0 @@ -using ConduitLLM.Configuration.Interfaces; -using ConduitLLM.Configuration.Repositories; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Http.Services; -using ConduitLLM.Http.Interfaces; -using Microsoft.AspNetCore.SignalR; - -public partial class Program -{ - public static void ConfigureSignalRServices(WebApplicationBuilder builder) - { - // Get Redis connection string from environment - var redisUrl = Environment.GetEnvironmentVariable("REDIS_URL"); - var redisConnectionString = Environment.GetEnvironmentVariable("CONDUIT_REDIS_CONNECTION_STRING"); - - if (!string.IsNullOrEmpty(redisUrl)) - { - try - { - redisConnectionString = ConduitLLM.Configuration.Utilities.RedisUrlParser.ParseRedisUrl(redisUrl); - } - catch - { - // Failed to parse REDIS_URL, will use legacy connection string if available - } - } - - // Register VirtualKeyHubFilter for SignalR authentication - builder.Services.AddScoped(); - - // Register rate limit cache service for SignalR - builder.Services.AddSingleton(); - builder.Services.AddHostedService(provider => - provider.GetRequiredService()); - - // Register SignalR rate limit filter - builder.Services.AddSingleton(); - - // Register SignalR metrics - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - - // Register SignalR metrics filter - builder.Services.AddSingleton(); - - // Register SignalR error handling filter - builder.Services.AddSingleton(); - - // Register SignalR authentication service - builder.Services.AddScoped(); - - // Register Metrics Aggregation Service and Hub - builder.Services.AddSingleton(); - builder.Services.AddHostedService(sp => - (ConduitLLM.Http.Services.MetricsAggregationService)sp.GetRequiredService()); - - // Register Business Metrics Background Service - builder.Services.AddHostedService(); - - // Add SignalR for real-time navigation state updates - var signalRBuilder = builder.Services.AddSignalR(options => - { - options.EnableDetailedErrors = builder.Environment.IsDevelopment(); - options.ClientTimeoutInterval = TimeSpan.FromSeconds(60); - options.KeepAliveInterval = TimeSpan.FromSeconds(30); - options.MaximumReceiveMessageSize = 32 * 1024; // 32KB - options.StreamBufferCapacity = 10; - - // Add global filters - options.AddFilter(); - options.AddFilter(); - options.AddFilter(); - options.AddFilter(); - }); - - // Configure SignalR Redis backplane for horizontal scaling - // Use dedicated Redis connection string if available, otherwise fall back to main Redis connection - var signalRRedisConnectionString = builder.Configuration.GetConnectionString("RedisSignalR") ?? redisConnectionString; - if (!string.IsNullOrEmpty(signalRRedisConnectionString)) - { - signalRBuilder.AddStackExchangeRedis(signalRRedisConnectionString, options => - { - options.Configuration.ChannelPrefix = new StackExchange.Redis.RedisChannel("conduit_signalr:", StackExchange.Redis.RedisChannel.PatternMode.Literal); - options.Configuration.DefaultDatabase = 2; // Separate database for SignalR - }); - Console.WriteLine("[Conduit] SignalR configured with Redis backplane for horizontal scaling"); - } - else - { - Console.WriteLine("[Conduit] SignalR configured without Redis backplane (single-instance mode)"); - } - - // Register navigation state notification service - builder.Services.AddSingleton(); - - // Register settings refresh service for runtime configuration updates - builder.Services.AddSingleton(); - - // MediaLifecycleRepository removed - consolidated into MediaRecordRepository - // Migration: 20250827194408_ConsolidateMediaTables.cs - - // Register video generation notification service - builder.Services.AddSingleton(); - - // Register image generation notification service - builder.Services.AddSingleton(); - - // Register unified task notification service - builder.Services.AddSingleton(); - - // Register virtual key management notification service - builder.Services.AddSingleton(); - - // Register usage analytics notification service - builder.Services.AddSingleton(); - - // Model discovery notification services removed - capabilities now come from ModelProviderMapping - - // Register billing alerting service for critical failure notifications - builder.Services.AddSingleton(); - - // Register Redis circuit breaker configuration - builder.Services.Configure( - builder.Configuration.GetSection("RedisCircuitBreaker")); - - // Register Redis circuit breaker service - builder.Services.AddSingleton(); - - // Register batch spend update service for optimized Virtual Key operations - builder.Services.AddSingleton(serviceProvider => - { - var logger = serviceProvider.GetRequiredService>(); - var serviceScopeFactory = serviceProvider.GetRequiredService(); - var redisConnectionFactory = serviceProvider.GetRequiredService(); - var options = serviceProvider.GetRequiredService>(); - var alertingService = serviceProvider.GetRequiredService(); - var circuitBreaker = serviceProvider.GetService(); - var batchService = new ConduitLLM.Configuration.Services.BatchSpendUpdateService(serviceScopeFactory, redisConnectionFactory, options, logger, alertingService, circuitBreaker); - - // Wire up cache invalidation event if Redis cache is available - var cache = serviceProvider.GetService(); - if (cache != null) - { - batchService.SpendUpdatesCompleted += async (keyHashes) => - { - try - { - await cache.InvalidateVirtualKeysAsync(keyHashes); - logger.LogDebug("Cache invalidated for {Count} Virtual Keys after batch spend update", keyHashes.Length); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to invalidate cache after batch spend update"); - } - }; - } - - return batchService; - }); - builder.Services.AddSingleton(serviceProvider => - serviceProvider.GetRequiredService()); - builder.Services.AddHostedService(serviceProvider => - serviceProvider.GetRequiredService()); - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/README.md b/ConduitLLM.Http/README.md deleted file mode 100644 index 95e170d0b..000000000 --- a/ConduitLLM.Http/README.md +++ /dev/null @@ -1,206 +0,0 @@ -# ConduitLLM.Http - -## Overview - -`ConduitLLM.Http` is the HTTP API backend for the Conduit solution (`Conduit.sln`). It exposes a unified, OpenAI-compatible REST API for interacting with multiple Large Language Model (LLM) providers such as OpenAI, Anthropic, Azure OpenAI, Gemini, Cohere, and others. It acts as the main programmatic interface for client applications and the ConduitLLM WebUI frontend. - -## Role in the Conduit Solution - -- **Conduit.sln**: The overall solution file tying together all Conduit sub-projects. -- **ConduitLLM.Http**: This project. Provides the HTTP API for LLM access, model routing, API key/virtual key management, and provider abstraction. -- **ConduitLLM.WebUI**: The web-based frontend for interactive LLM usage, configuration, and administration. Communicates with this API. -- **ConduitLLM.Core**: Shared logic, models, and interfaces for LLM operations, used by both Http and WebUI. -- **ConduitLLM.Configuration**: Handles configuration persistence (database, environment, files) and settings management. -- **ConduitLLM.Providers**: Implements provider-specific logic for different LLM backends. -- **ConduitLLM.Tests**: Unit and integration tests for all components. - -## Key Features - -- **OpenAI-compatible REST API** (`/v1/chat/completions`) -- **Multiple LLM Provider Support** (OpenAI, Anthropic, Azure, Gemini, Cohere, etc.) -- **Model Routing**: Map model names to provider/model pairs -- **API Key & Virtual Key Management**: Supports both provider keys and Conduit-managed virtual keys -- **Streaming and non-streaming responses** -- **Swagger/OpenAPI documentation** (enabled in development) -- **Database-backed and file-based configuration** - -## Configuration - -### Environment Variables - -- `HttpApiHttpPort` (default: 5000): HTTP port for the API -- `HttpApiHttpsPort` (default: 5003): HTTPS port for the API -- `ASPNETCORE_URLS`: Sets the actual listen URLs (usually http://+:80 for Docker) -- Provider API keys can be set via environment variables or `appsettings.json` (see below) - -### appsettings.json - -- `ConnectionStrings:DefaultConnection`: Path to the configuration database (default: `../configuration.db`) -- `Conduit:ModelProviderMapping`: Maps logical model names to provider/model pairs -- `Conduit:ProviderCredentials`: Stores API keys and endpoints for each provider - -Example snippet: -```json -"Conduit": { - "ModelProviderMapping": { - "gpt-4-proxy-example": "openai/gpt-4", - "claude-3-opus-proxy-example": "anthropic/claude-3-opus-20240229" - }, - "ProviderCredentials": { - "OpenAI": { - "ApiKey": "YOUR_OPENAI_API_KEY_OR_USE_ENV_VAR" - }, - "Anthropic": { - "ApiKey": "YOUR_ANTHROPIC_API_KEY_OR_USE_ENV_VAR" - } - // ... other providers ... - } -} -``` - -### Database Configuration - -This service supports both Postgres and SQLite, configured via environment variables ONLY (no appsettings.json required): - -- **Postgres:** - - Set `DATABASE_URL` (e.g., `postgresql://user:password@host:5432/database`) -- **SQLite:** - - Set `CONDUIT_SQLITE_PATH` (e.g., `/data/ConduitConfig.db`) - -No other DB-related variables are needed. The service will auto-detect the provider. - -### Docker & Port Management - -- Designed for containerization. Ports are set via environment variables for flexible deployment. -- When using Docker Compose, ports are configured in the docker-compose.yml file. - -## Persistent Database Storage (Docker) - -To ensure your SQLite database persists across container restarts, set the `CONDUIT_SQLITE_PATH` environment variable and mount a persistent volume: - -```yaml -environment: - - CONDUIT_SQLITE_PATH=/data/conduit.db -volumes: - - ./my-data:/data -``` - -This will store the database at `/data/conduit.db` inside the container, mapped to your host directory `./my-data`. - -### Best Practices -- Always use a persistent volume for `/data` in production or testing containers. -- Make sure the volume is writable by the app user (check Docker UID/GID). -- Use `CONDUIT_SQLITE_PATH` for clarity and to avoid accidental data loss. - -### Troubleshooting -- **File not created or missing**: Check that the volume is mounted and the path is correct. -- **Permission denied**: Ensure the directory and file are writable by the container user. -- **Database is read-only**: The file or directory may be mounted as read-only or lack write permissions. -- **App uses wrong database file**: Double-check environment variable spelling and container/service environment. - -For more help, see the Database Status page in the WebUI. - -## Running the API - -1. **With .NET CLI:** - ```bash - export HttpApiHttpPort=5000 - export HttpApiHttpsPort=5003 - dotnet run --project ConduitLLM.Http - ``` -2. **With Docker:** - ```bash - docker build -t conduitllm-http . - docker run -e HttpApiHttpPort=5000 -e HttpApiHttpsPort=5003 -p 5000:5000 -p 5003:5003 conduitllm-http - ``` -3. **With Docker Compose:** - ```bash - docker compose up -d - ``` - -## API Usage - -- Main endpoint: `POST /v1/chat/completions` (OpenAI-compatible) -- Supports both standard and streaming responses -- Requires API key (provider or virtual key) in the `Authorization: Bearer ...` header -- See Swagger UI at `/swagger` (in development mode) - -## Virtual Keys - -- Virtual keys (`condt_...`) are managed by Conduit and can be used instead of provider keys. -- Spend tracking and access control are supported for virtual keys. - -## Refreshing Configuration - -- POST `/admin/refresh-configuration` endpoint reloads provider credentials and model mappings from the database. - -## Health Checks - -The API provides health check endpoints for monitoring and container orchestration: - -### Database Health Endpoint - -- `GET /health/db`: Checks database connectivity and migration status -- Returns: - - **200 OK**: Database is reachable and all migrations are applied - - **503 Service Unavailable**: Database is unreachable or migrations are pending - - **500 Internal Server Error**: Unexpected error occurred during health check - -The response is a JSON payload with the following structure: -```json -{ - "status": "healthy", // or "unhealthy" - "timestamp": "2025-04-30T07:30:04.5478Z", - "details": null // Contains error information when unhealthy -} -``` - -### Using with Docker & Kubernetes - -For Docker Compose health checks: -```yaml -healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5000/health/db"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s -``` - -For Kubernetes readiness and liveness probes: -```yaml -readinessProbe: - httpGet: - path: /health/db - port: 5000 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 3 - failureThreshold: 3 - -livenessProbe: - httpGet: - path: /health/db - port: 5000 - initialDelaySeconds: 60 - periodSeconds: 30 - timeoutSeconds: 5 - failureThreshold: 3 -``` - -## Development Notes - -- Uses ASP.NET Core Minimal APIs -- Configuration is loaded from both `appsettings.json` and the configuration database -- Provider credentials and model mappings can be managed via the WebUI or directly in the database - -## Troubleshooting - -- When using Docker Compose, use `docker compose down` to stop all services -- Logs are output to the console by default (configurable in `appsettings.json`) -- Use `docker compose logs` to view container logs - -## License - -See the root of the repository for license information. - diff --git a/ConduitLLM.Http/Services/DistributedMediaSchedulerService.cs b/ConduitLLM.Http/Services/DistributedMediaSchedulerService.cs deleted file mode 100644 index 72e366fcf..000000000 --- a/ConduitLLM.Http/Services/DistributedMediaSchedulerService.cs +++ /dev/null @@ -1,185 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Options; -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Configuration.Options; - -namespace ConduitLLM.Http.Services -{ - /// - /// Distributed scheduler service for media lifecycle management. - /// Uses Redis-based leader election to ensure only one scheduler runs across multiple instances. - /// - public class DistributedMediaSchedulerService : BackgroundService - { - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly IDistributedLockService _lockService; - private readonly MediaLifecycleOptions _options; - private readonly IConfiguration _configuration; - private readonly ILogger _logger; - private readonly string _instanceId; - - public DistributedMediaSchedulerService( - IServiceScopeFactory serviceScopeFactory, - IDistributedLockService lockService, - IOptions options, - IConfiguration configuration, - ILogger logger) - { - _serviceScopeFactory = serviceScopeFactory; - _lockService = lockService; - _options = options.Value; - _configuration = configuration; - _logger = logger; - _instanceId = Guid.NewGuid().ToString("N")[..8]; // Short instance ID for logging - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - // Check if this instance should run the scheduler - var schedulerMode = _options.SchedulerMode; - var currentRole = _configuration["ServiceRole"] ?? "CoreApi"; - - if (!ShouldRunScheduler(schedulerMode, currentRole)) - { - _logger.LogInformation( - "Media scheduler disabled for instance {InstanceId} - Mode: {Mode}, Role: {Role}", - _instanceId, schedulerMode, currentRole); - return; - } - - _logger.LogInformation( - "Media scheduler service starting on instance {InstanceId} - Mode: {Mode}, Role: {Role}", - _instanceId, schedulerMode, currentRole); - - while (!stoppingToken.IsCancellationRequested) - { - try - { - await AttemptSchedulerExecution(stoppingToken); - - // Wait for the configured interval before next attempt - await Task.Delay( - TimeSpan.FromMinutes(_options.ScheduleIntervalMinutes), - stoppingToken); - } - catch (OperationCanceledException) - { - // Normal shutdown - break; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Unexpected error in scheduler service on instance {InstanceId}", - _instanceId); - - // Wait before retrying to avoid tight error loops - await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); - } - } - - _logger.LogInformation( - "Media scheduler service stopping on instance {InstanceId}", - _instanceId); - } - - private async Task AttemptSchedulerExecution(CancellationToken stoppingToken) - { - // TODO: Implement Redis Sentinel or Cluster for high availability - // TODO: Add secondary lock mechanism using database advisory locks - // TODO: Implement heartbeat mechanism to detect zombie locks - // TODO: Add metrics to detect split-brain scenarios - - var lockKey = "media:scheduler:leader"; - var lockDuration = TimeSpan.FromMinutes(5); - - // Try to acquire the leader lock - using var lockHandle = await _lockService.AcquireLockAsync( - lockKey, - lockDuration, - stoppingToken); - - if (lockHandle == null) - { - _logger.LogDebug( - "Instance {InstanceId} could not acquire scheduler lock - another instance is leader", - _instanceId); - return; - } - - _logger.LogInformation( - "Instance {InstanceId} acquired scheduler leadership for {Duration}", - _instanceId, lockDuration); - - try - { - await RunScheduledCleanupAsync(stoppingToken); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error during scheduled cleanup on instance {InstanceId}", - _instanceId); - - // TODO: Implement dead letter queue for failing schedules - throw; - } - finally - { - _logger.LogInformation( - "Instance {InstanceId} releasing scheduler leadership", - _instanceId); - } - } - - private async Task RunScheduledCleanupAsync(CancellationToken stoppingToken) - { - // TODO: Implement lease renewal during long operations - // TODO: Implement checkpointing to resume after crash - - _logger.LogInformation( - "Instance {InstanceId} starting scheduled media cleanup run", - _instanceId); - - // Create a scope to resolve scoped services - using var scope = _serviceScopeFactory.CreateScope(); - var publishEndpoint = scope.ServiceProvider.GetRequiredService(); - - // Publish the schedule event that will trigger retention checks - var scheduleEvent = new MediaCleanupScheduleRequested( - DateTime.UtcNow, - _instanceId) - { - IsDryRun = _options.DryRunMode - }; - - await publishEndpoint.Publish(scheduleEvent, stoppingToken); - - _logger.LogInformation( - "Instance {InstanceId} published cleanup schedule event (DryRun: {DryRun})", - _instanceId, _options.DryRunMode); - } - - private bool ShouldRunScheduler(string schedulerMode, string currentRole) - { - return schedulerMode?.ToLowerInvariant() switch - { - "disabled" => false, - "adminapi" => currentRole.Equals("AdminApi", StringComparison.OrdinalIgnoreCase), - "coreapi" => currentRole.Equals("CoreApi", StringComparison.OrdinalIgnoreCase), - "any" => true, - _ => false // Default to disabled for safety - }; - } - - public override async Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation( - "Media scheduler service received stop signal on instance {InstanceId}", - _instanceId); - - await base.StopAsync(cancellationToken); - } - } -} \ No newline at end of file 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/MetricsAggregationService.cs b/ConduitLLM.Http/Services/MetricsAggregationService.cs deleted file mode 100644 index 874b98e42..000000000 --- a/ConduitLLM.Http/Services/MetricsAggregationService.cs +++ /dev/null @@ -1,252 +0,0 @@ -using Microsoft.AspNetCore.SignalR; -using StackExchange.Redis; -using ConduitLLM.Configuration.DTOs.Metrics; -using ConduitLLM.Http.Hubs; -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Interfaces; - -namespace ConduitLLM.Http.Services -{ - /// - /// Service that aggregates metrics from various sources and provides them to the dashboard. - /// - public partial class MetricsAggregationService : BackgroundService, IMetricsAggregationService - { - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly IHubContext _hubContext; - private readonly TimeSpan _updateInterval = TimeSpan.FromSeconds(5); - private readonly Dictionary _historicalData = new(); - private readonly object _dataLock = new(); - private MetricsSnapshot? _lastSnapshot; - - // Alert thresholds - private const double ErrorRateThreshold = 5.0; // 5% error rate - private const double ResponseTimeThreshold = 5000; // 5 seconds - private const double CpuUsageThreshold = 80.0; // 80% CPU - private const double MemoryUsageThreshold = 85.0; // 85% memory - private const int QueueDepthThreshold = 1000; // 1000 messages - - public MetricsAggregationService( - IServiceProvider serviceProvider, - ILogger logger, - IHubContext hubContext) - { - _serviceProvider = serviceProvider; - _logger = logger; - _hubContext = hubContext; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - _logger.LogInformation("Metrics aggregation service starting..."); - - while (!stoppingToken.IsCancellationRequested) - { - try - { - var snapshot = await CollectMetricsSnapshotAsync(); - _lastSnapshot = snapshot; - - // Store historical data - StoreHistoricalData(snapshot); - - // Broadcast to all subscribers - await _hubContext.Clients.Group("metrics-subscribers") - .SendAsync("MetricsSnapshot", snapshot, stoppingToken); - - // Send targeted updates - await SendTargetedUpdates(snapshot, stoppingToken); - - // Check for alerts - await CheckAndSendAlerts(snapshot, stoppingToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in metrics aggregation"); - } - - await Task.Delay(_updateInterval, stoppingToken); - } - - _logger.LogInformation("Metrics aggregation service stopped"); - } - - public async Task GetCurrentSnapshotAsync() - { - if (_lastSnapshot != null && (DateTime.UtcNow - _lastSnapshot.Timestamp).TotalSeconds < 10) - { - return _lastSnapshot; - } - - return await CollectMetricsSnapshotAsync(); - } - - private async Task CollectMetricsSnapshotAsync() - { - var snapshot = new MetricsSnapshot - { - Timestamp = DateTime.UtcNow - }; - - var tasks = new[] - { - Task.Run(() => CollectHttpMetrics(snapshot)), - Task.Run(() => CollectInfrastructureMetrics(snapshot)), - Task.Run(() => CollectBusinessMetrics(snapshot)), - Task.Run(() => CollectSystemMetrics(snapshot)), - Task.Run(() => CollectProviderHealth(snapshot)) - }; - - await Task.WhenAll(tasks); - - return snapshot; - } - - - - public async Task GetHistoricalMetricsAsync(HistoricalMetricsRequest request) - { - await Task.CompletedTask; // Make async - - lock (_dataLock) - { - var response = new HistoricalMetricsResponse - { - StartTime = request.StartTime, - EndTime = request.EndTime, - Interval = request.Interval, - Series = new List() - }; - - foreach (var metricName in request.MetricNames) - { - if (_historicalData.TryGetValue(metricName, out var series)) - { - var filteredSeries = new MetricsSeries - { - MetricName = series.MetricName, - Label = series.Label, - DataPoints = series.DataPoints - .Where(p => p.Timestamp >= request.StartTime && p.Timestamp <= request.EndTime) - .ToList() - }; - - if (filteredSeries.DataPoints.Count() > 0) - { - response.Series.Add(filteredSeries); - } - } - } - - return response; - } - } - - public async Task> GetActiveAlertsAsync() - { - var snapshot = await GetCurrentSnapshotAsync(); - var alerts = new List(); - - // Check various thresholds and generate alerts - if (snapshot.Http.ErrorRate > ErrorRateThreshold) - { - alerts.Add(new MetricAlert - { - Id = "http-error-rate", - Severity = "critical", - MetricName = "HTTP Error Rate", - Message = $"HTTP error rate exceeds threshold", - CurrentValue = snapshot.Http.ErrorRate, - Threshold = ErrorRateThreshold, - TriggeredAt = DateTime.UtcNow, - IsActive = true - }); - } - - return alerts; - } - - public async Task> CheckProviderHealthAsync(string? providerName) - { - var snapshot = await GetCurrentSnapshotAsync(); - - if (string.IsNullOrEmpty(providerName)) - { - return snapshot.ProviderHealth; - } - - return snapshot.ProviderHealth - .Where(p => p.ProviderType.ToString().Equals(providerName, StringComparison.OrdinalIgnoreCase)) - .ToList(); - } - - public async Task> GetTopVirtualKeysAsync(string metric, int count) - { - var snapshot = await GetCurrentSnapshotAsync(); - - return metric.ToLower() switch - { - "requests" => snapshot.Business.TopVirtualKeys - .OrderByDescending(k => k.RequestsPerMinute) - .Take(count) - .ToList(), - "spend" => snapshot.Business.TopVirtualKeys - .OrderByDescending(k => k.TotalSpend) - .Take(count) - .ToList(), - "budget" => snapshot.Business.TopVirtualKeys - .OrderByDescending(k => k.BudgetUtilization) - .Take(count) - .ToList(), - _ => snapshot.Business.TopVirtualKeys.Take(count).ToList() - }; - } - - - /// - /// Checks provider health status. - /// - /// - /// Provider health monitoring has been removed. This method now returns - /// all enabled providers as healthy. - /// - public async Task> CheckProviderHealthAsync(ProviderType? providerType) - { - using var scope = _serviceProvider.CreateScope(); - var providerRepository = scope.ServiceProvider.GetRequiredService(); - - var healthStatuses = new List(); - - // Get all providers - var providers = await providerRepository.GetAllAsync(); - - // Group providers by type - var providersByType = providers - .Where(p => p.IsEnabled) - .GroupBy(p => p.ProviderType) - .ToDictionary(g => g.Key, g => g.ToList()); - - foreach (var typeGroup in providersByType) - { - // Skip if filtering by type and this isn't the requested type - if (providerType.HasValue && typeGroup.Key != providerType.Value) - continue; - - // All enabled providers are considered healthy - healthStatuses.Add(new ProviderHealthStatus - { - ProviderType = typeGroup.Key, - Status = "healthy", - AverageLatency = 0, - LastSuccessfulRequest = DateTime.UtcNow, - ErrorRate = 0, - IsEnabled = true, - AvailableModels = 0 - }); - } - - return healthStatuses; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Services/NavigationStateNotificationService.cs b/ConduitLLM.Http/Services/NavigationStateNotificationService.cs deleted file mode 100644 index 2a4cf22ff..000000000 --- a/ConduitLLM.Http/Services/NavigationStateNotificationService.cs +++ /dev/null @@ -1,125 +0,0 @@ -using Microsoft.AspNetCore.SignalR; -using ConduitLLM.Http.Hubs; -using ConduitLLM.Configuration.DTOs.SignalR; -namespace ConduitLLM.Http.Services -{ - /// - /// Service for sending real-time navigation state updates through SignalR - /// - public interface INavigationStateNotificationService - { - /// - /// Notifies all connected clients of a model mapping change - /// - Task NotifyModelMappingChangedAsync(int mappingId, string modelAlias, string changeType); - - /// - /// Notifies all connected clients of a provider health change - /// - Task NotifyProviderHealthChangedAsync(int providerId, string providerName, bool isHealthy, string status); - - - /// - /// Notifies specific model subscribers of availability change - /// - Task NotifyModelAvailabilityChangedAsync(string modelId, bool isAvailable); - } - - /// - /// Implementation of navigation state notification service using SignalR - /// - public class NavigationStateNotificationService : INavigationStateNotificationService - { - private readonly IHubContext _hubContext; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the NavigationStateNotificationService - /// - /// SignalR hub context for SystemNotificationHub - /// Logger instance - public NavigationStateNotificationService( - IHubContext hubContext, - ILogger logger) - { - _hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public async Task NotifyModelMappingChangedAsync(int mappingId, string modelAlias, string changeType) - { - try - { - var notification = new ModelMappingNotification - { - MappingId = mappingId, - ModelAlias = modelAlias, - ChangeType = changeType, - Priority = NotificationPriority.Medium - }; - - await _hubContext.Clients.All.SendAsync("OnModelMappingChanged", notification); - - _logger.LogDebug("Sent model mapping change notification for {ModelAlias} ({ChangeType})", modelAlias, changeType); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to send model mapping change notification for {ModelAlias}", modelAlias); - } - } - - /// - public async Task NotifyProviderHealthChangedAsync(int providerId, string providerName, bool isHealthy, string status) - { - try - { - // Convert bool isHealthy to HealthStatus enum - var healthStatus = isHealthy ? HealthStatus.Healthy : HealthStatus.Unhealthy; - if (status.Contains("degraded", StringComparison.OrdinalIgnoreCase)) - { - healthStatus = HealthStatus.Degraded; - } - - var notification = new ProviderHealthNotification - { - ProviderId = providerId, - ProviderName = providerName, - Status = healthStatus.ToString(), - Priority = healthStatus == HealthStatus.Unhealthy ? NotificationPriority.High : NotificationPriority.Medium - }; - - await _hubContext.Clients.All.SendAsync("OnProviderHealthChanged", notification); - - _logger.LogDebug("Sent provider health change notification for {ProviderName} (ID: {ProviderId}, Healthy: {IsHealthy})", providerName, providerId, isHealthy); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to send provider health change notification for {ProviderName} (ID: {ProviderId})", providerName, providerId); - } - } - - - /// - public async Task NotifyModelAvailabilityChangedAsync(string modelId, bool isAvailable) - { - try - { - var notification = new ModelAvailabilityNotification - { - ModelId = modelId, - IsAvailable = isAvailable, - Priority = isAvailable ? NotificationPriority.Low : NotificationPriority.Medium - }; - - await _hubContext.Clients.All.SendAsync("OnModelAvailabilityChanged", notification); - - _logger.LogDebug("Sent model availability change notification for {ModelId} (Available: {IsAvailable})", modelId, isAvailable); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to send model availability change notification for {ModelId}", modelId); - } - } - } -} \ 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/SecurityService.IpFiltering.cs b/ConduitLLM.Http/Services/SecurityService.IpFiltering.cs deleted file mode 100644 index d2d364f6a..000000000 --- a/ConduitLLM.Http/Services/SecurityService.IpFiltering.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System.Net; - -using ConduitLLM.Http.Interfaces; - -namespace ConduitLLM.Http.Services -{ - public partial class SecurityService - { - private async Task CheckIpFilterAsync(string ipAddress) - { - // Check if it's a private IP and we allow private IPs - if (_options.IpFiltering.AllowPrivateIps) - { - if (IsPrivateIp(ipAddress)) - { - _logger.LogDebug("Private/Intranet IP {IpAddress} is automatically allowed", ipAddress); - return new SecurityCheckResult { IsAllowed = true }; - } - } - - // Check environment variable based filters - var isInWhitelist = _options.IpFiltering.Whitelist.Any(rule => IsIpInRange(ipAddress, rule)); - var isInBlacklist = _options.IpFiltering.Blacklist.Any(rule => IsIpInRange(ipAddress, rule)); - - var isAllowed = _options.IpFiltering.Mode.ToLower() == "restrictive" - ? isInWhitelist && !isInBlacklist - : !isInBlacklist; - - if (!isAllowed) - { - _logger.LogWarning("IP {IpAddress} blocked by IP filter rules", ipAddress); - return new SecurityCheckResult - { - IsAllowed = false, - Reason = "IP address not allowed", - StatusCode = 403 - }; - } - - // Also check database-based IP filters - using (var scope = _serviceProvider.CreateScope()) - { - var ipFilterService = scope.ServiceProvider.GetRequiredService(); - var isAllowedByDb = await ipFilterService.IsIpAllowedAsync(ipAddress); - if (!isAllowedByDb) - { - _logger.LogWarning("IP {IpAddress} blocked by database IP filter", ipAddress); - return new SecurityCheckResult - { - IsAllowed = false, - Reason = "IP address not allowed", - StatusCode = 403 - }; - } - } - - return new SecurityCheckResult { IsAllowed = true }; - } - - private bool IsPrivateIp(string ipAddress) - { - if (!IPAddress.TryParse(ipAddress, out var ip)) - return false; - - // Check loopback - if (IPAddress.IsLoopback(ip)) - return true; - - // Check private ranges - if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) - { - var ipBytes = ip.GetAddressBytes(); - - // Check private ranges - if (ipBytes[0] == 10 || // 10.0.0.0/8 - (ipBytes[0] == 172 && ipBytes[1] >= 16 && ipBytes[1] <= 31) || // 172.16.0.0/12 - (ipBytes[0] == 192 && ipBytes[1] == 168) || // 192.168.0.0/16 - (ipBytes[0] == 169 && ipBytes[1] == 254)) // 169.254.0.0/16 (link-local) - { - return true; - } - } - - return false; - } - - private bool IsIpInRange(string ipAddress, string rule) - { - // Simple IP match - if (ipAddress == rule) - return true; - - // CIDR range check - if (rule.Contains('/')) - { - return IsIpInCidrRange(ipAddress, rule); - } - - return false; - } - - private bool IsIpInCidrRange(string ipAddress, string cidrRange) - { - try - { - var parts = cidrRange.Split('/'); - if (parts.Length != 2) - return false; - - if (!IPAddress.TryParse(ipAddress, out var ip)) - return false; - - if (!IPAddress.TryParse(parts[0], out var baseAddress)) - return false; - - if (!int.TryParse(parts[1], out var prefixLength)) - return false; - - // Only support IPv4 for now - if (ip.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork || - baseAddress.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) - return false; - - var ipBytes = ip.GetAddressBytes(); - var baseBytes = baseAddress.GetAddressBytes(); - - // Calculate the mask - var maskBytes = new byte[4]; - for (int i = 0; i < 4; i++) - { - if (prefixLength >= 8) - { - maskBytes[i] = 0xFF; - prefixLength -= 8; - } - else if (prefixLength > 0) - { - maskBytes[i] = (byte)(0xFF << (8 - prefixLength)); - prefixLength = 0; - } - else - { - maskBytes[i] = 0x00; - } - } - - // Check if the IP is in the range - for (int i = 0; i < 4; i++) - { - if ((ipBytes[i] & maskBytes[i]) != (baseBytes[i] & maskBytes[i])) - return false; - } - - return true; - } - catch - { - return false; - } - } - - private bool IsPathExcluded(string path, List excludedPaths) - { - return excludedPaths.Any(excluded => path.StartsWith(excluded, StringComparison.OrdinalIgnoreCase)); - } - - private string GetClientIpAddress(HttpContext context) - { - // Check X-Forwarded-For header first (for reverse proxies) - var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); - if (!string.IsNullOrEmpty(forwardedFor)) - { - // Take the first IP in the chain - var ip = forwardedFor.Split(',').First().Trim(); - if (IPAddress.TryParse(ip, out _)) - { - return ip; - } - } - - // Check X-Real-IP header - var realIp = context.Request.Headers["X-Real-IP"].FirstOrDefault(); - if (!string.IsNullOrEmpty(realIp) && IPAddress.TryParse(realIp, out _)) - { - return realIp; - } - - // Fall back to direct connection IP - return context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Services/SignalRAcknowledgmentService.cs b/ConduitLLM.Http/Services/SignalRAcknowledgmentService.cs deleted file mode 100644 index 9f20b7525..000000000 --- a/ConduitLLM.Http/Services/SignalRAcknowledgmentService.cs +++ /dev/null @@ -1,378 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Http.Models; - -namespace ConduitLLM.Http.Services -{ - /// - /// Service that manages message acknowledgments for SignalR - /// - public interface ISignalRAcknowledgmentService - { - /// - /// Registers a message for acknowledgment tracking - /// - Task RegisterMessageAsync(SignalRMessage message, string connectionId, string hubName, string methodName, TimeSpan? timeout = null); - - /// - /// Acknowledges a message by its ID - /// - Task AcknowledgeMessageAsync(string messageId, string connectionId); - - /// - /// Negatively acknowledges a message by its ID - /// - Task NackMessageAsync(string messageId, string connectionId, string? errorMessage = null); - - /// - /// Gets the status of a message acknowledgment - /// - Task GetMessageStatusAsync(string messageId); - - /// - /// Gets all pending acknowledgments for a connection - /// - Task> GetPendingAcknowledgmentsAsync(string connectionId); - - /// - /// Cleans up acknowledgments for a disconnected client - /// - Task CleanupConnectionAsync(string connectionId); - } - - /// - /// Implementation of SignalR acknowledgment service - /// - public class SignalRAcknowledgmentService : ISignalRAcknowledgmentService, IHostedService, IDisposable - { - private readonly ILogger _logger; - private readonly IConfiguration _configuration; - private readonly ConcurrentDictionary _pendingAcknowledgments = new(); - private readonly ConcurrentDictionary> _connectionMessageIds = new(); - private Timer? _cleanupTimer; - private readonly TimeSpan _defaultTimeout; - private readonly TimeSpan _cleanupInterval; - private readonly int _maxRetryAttempts; - - public SignalRAcknowledgmentService( - ILogger logger, - IConfiguration configuration) - { - _logger = logger; - _configuration = configuration; - - _defaultTimeout = TimeSpan.FromSeconds(configuration.GetValue("SignalR:Acknowledgment:TimeoutSeconds", 30)); - _cleanupInterval = TimeSpan.FromMinutes(configuration.GetValue("SignalR:Acknowledgment:CleanupIntervalMinutes", 5)); - _maxRetryAttempts = configuration.GetValue("SignalR:Acknowledgment:MaxRetryAttempts", 3); - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("SignalR Acknowledgment Service starting"); - - _cleanupTimer = new Timer( - CleanupExpiredAcknowledgments, - null, - _cleanupInterval, - _cleanupInterval); - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("SignalR Acknowledgment Service stopping"); - - _cleanupTimer?.Change(Timeout.Infinite, 0); - - // Cancel all pending acknowledgments - foreach (var pending in _pendingAcknowledgments.Values) - { - pending.TimeoutTokenSource?.Cancel(); - pending.CompletionSource.TrySetCanceled(); - } - - return Task.CompletedTask; - } - - public Task RegisterMessageAsync( - SignalRMessage message, - string connectionId, - string hubName, - string methodName, - TimeSpan? timeout = null) - { - var effectiveTimeout = timeout ?? _defaultTimeout; - var timeoutAt = DateTime.UtcNow.Add(effectiveTimeout); - - var pending = new PendingAcknowledgment - { - Message = message, - ConnectionId = connectionId, - HubName = hubName, - MethodName = methodName, - TimeoutAt = timeoutAt, - TimeoutTokenSource = new CancellationTokenSource() - }; - - if (!_pendingAcknowledgments.TryAdd(message.MessageId, pending)) - { - _logger.LogWarning("Message {MessageId} already registered for acknowledgment", message.MessageId); - throw new InvalidOperationException($"Message {message.MessageId} already registered"); - } - - // Track message ID by connection - _connectionMessageIds.AddOrUpdate( - connectionId, - new ConcurrentBag { message.MessageId }, - (_, bag) => { bag.Add(message.MessageId); return bag; }); - - // Schedule timeout handling - _ = Task.Run(async () => - { - try - { - await Task.Delay(effectiveTimeout, pending.TimeoutTokenSource.Token); - await HandleTimeoutAsync(message.MessageId); - } - catch (TaskCanceledException) - { - // Expected when acknowledgment is received before timeout - } - }); - - _logger.LogDebug( - "Registered message {MessageId} for acknowledgment on {HubName}.{MethodName} to {ConnectionId}, timeout at {TimeoutAt}", - message.MessageId, hubName, methodName, connectionId, timeoutAt); - - return Task.FromResult(pending); - } - - public Task AcknowledgeMessageAsync(string messageId, string connectionId) - { - if (!_pendingAcknowledgments.TryGetValue(messageId, out var pending)) - { - _logger.LogWarning("Attempted to acknowledge unknown message {MessageId}", messageId); - return Task.FromResult(false); - } - - if (pending.ConnectionId != connectionId) - { - _logger.LogWarning( - "Connection {ConnectionId} attempted to acknowledge message {MessageId} sent to {OriginalConnectionId}", - connectionId, messageId, pending.ConnectionId); - return Task.FromResult(false); - } - - pending.Status = AcknowledgmentStatus.Acknowledged; - pending.AcknowledgedAt = DateTime.UtcNow; - pending.TimeoutTokenSource?.Cancel(); - pending.CompletionSource.TrySetResult(true); - - _logger.LogDebug( - "Message {MessageId} acknowledged by {ConnectionId}, RTT: {RoundTripTime}ms", - messageId, connectionId, pending.RoundTripTime?.TotalMilliseconds ?? 0); - - // Clean up after a delay - _ = Task.Run(async () => - { - await Task.Delay(TimeSpan.FromMinutes(1)); - _pendingAcknowledgments.TryRemove(messageId, out _); - RemoveMessageIdFromConnection(connectionId, messageId); - }); - - return Task.FromResult(true); - } - - public Task NackMessageAsync(string messageId, string connectionId, string? errorMessage = null) - { - if (!_pendingAcknowledgments.TryGetValue(messageId, out var pending)) - { - _logger.LogWarning("Attempted to NACK unknown message {MessageId}", messageId); - return Task.FromResult(false); - } - - if (pending.ConnectionId != connectionId) - { - _logger.LogWarning( - "Connection {ConnectionId} attempted to NACK message {MessageId} sent to {OriginalConnectionId}", - connectionId, messageId, pending.ConnectionId); - return Task.FromResult(false); - } - - pending.Status = AcknowledgmentStatus.NegativelyAcknowledged; - pending.ErrorMessage = errorMessage; - pending.AcknowledgedAt = DateTime.UtcNow; - pending.TimeoutTokenSource?.Cancel(); - pending.CompletionSource.TrySetResult(false); - - _logger.LogWarning( - "Message {MessageId} negatively acknowledged by {ConnectionId}: {ErrorMessage}", - messageId, connectionId, errorMessage ?? "No error message provided"); - - // Should retry if under retry limit and message is critical - if (pending.Message.IsCritical && pending.Message.RetryCount < _maxRetryAttempts) - { - _logger.LogInformation( - "Queueing critical message {MessageId} for retry (attempt {RetryCount}/{MaxRetries})", - messageId, pending.Message.RetryCount + 1, _maxRetryAttempts); - // Message will be picked up by the message queue service for retry - } - - return Task.FromResult(true); - } - - public Task GetMessageStatusAsync(string messageId) - { - if (_pendingAcknowledgments.TryGetValue(messageId, out var pending)) - { - return Task.FromResult(pending.Status); - } - return Task.FromResult(null); - } - - public Task> GetPendingAcknowledgmentsAsync(string connectionId) - { - var messageIds = _connectionMessageIds.TryGetValue(connectionId, out var bag) - ? bag.ToList() - : new List(); - - var pendingAcks = messageIds - .Select(id => _pendingAcknowledgments.TryGetValue(id, out var pending) ? pending : null) - .Where(p => p != null && p.Status == AcknowledgmentStatus.Pending) - .Cast(); - - return Task.FromResult(pendingAcks); - } - - public async Task CleanupConnectionAsync(string connectionId) - { - _logger.LogInformation("Cleaning up acknowledgments for disconnected connection {ConnectionId}", connectionId); - - if (!_connectionMessageIds.TryRemove(connectionId, out var messageIds)) - { - return; - } - - foreach (var messageId in messageIds) - { - if (_pendingAcknowledgments.TryGetValue(messageId, out var pending) && - pending.Status == AcknowledgmentStatus.Pending) - { - pending.Status = AcknowledgmentStatus.Failed; - pending.ErrorMessage = "Connection disconnected"; - pending.TimeoutTokenSource?.Cancel(); - pending.CompletionSource.TrySetResult(false); - - _logger.LogWarning( - "Message {MessageId} failed due to connection {ConnectionId} disconnect", - messageId, connectionId); - } - } - - await Task.CompletedTask; - } - - private async Task HandleTimeoutAsync(string messageId) - { - if (!_pendingAcknowledgments.TryGetValue(messageId, out var pending)) - { - return; - } - - if (pending.Status != AcknowledgmentStatus.Pending) - { - return; - } - - pending.Status = AcknowledgmentStatus.TimedOut; - pending.CompletionSource.TrySetResult(false); - - _logger.LogWarning( - "Message {MessageId} timed out after {Timeout}ms on {HubName}.{MethodName} to {ConnectionId}", - messageId, - (DateTime.UtcNow - pending.SentAt).TotalMilliseconds, - pending.HubName, - pending.MethodName, - pending.ConnectionId); - - // Should retry if under retry limit and message is critical - if (pending.Message.IsCritical && pending.Message.RetryCount < _maxRetryAttempts) - { - _logger.LogInformation( - "Queueing critical message {MessageId} for retry after timeout (attempt {RetryCount}/{MaxRetries})", - messageId, pending.Message.RetryCount + 1, _maxRetryAttempts); - // Message will be picked up by the message queue service for retry - } - - await Task.CompletedTask; - } - - private void CleanupExpiredAcknowledgments(object? state) - { - try - { - var cutoffTime = DateTime.UtcNow.AddHours(-1); - var expiredKeys = _pendingAcknowledgments - .Where(kvp => kvp.Value.SentAt < cutoffTime && - kvp.Value.Status != AcknowledgmentStatus.Pending) - .Select(kvp => kvp.Key) - .ToList(); - - foreach (var key in expiredKeys) - { - if (_pendingAcknowledgments.TryRemove(key, out var pending)) - { - RemoveMessageIdFromConnection(pending.ConnectionId, key); - pending.TimeoutTokenSource?.Dispose(); - } - } - - if (expiredKeys.Count() > 0) - { - _logger.LogDebug("Cleaned up {Count} expired acknowledgments", expiredKeys.Count); - } - - // Also check for messages that have expired - var expiredMessages = _pendingAcknowledgments - .Where(kvp => kvp.Value.Message.IsExpired && - kvp.Value.Status == AcknowledgmentStatus.Pending) - .Select(kvp => kvp.Key) - .ToList(); - - foreach (var key in expiredMessages) - { - if (_pendingAcknowledgments.TryGetValue(key, out var pending)) - { - pending.Status = AcknowledgmentStatus.Expired; - pending.TimeoutTokenSource?.Cancel(); - pending.CompletionSource.TrySetResult(false); - _logger.LogWarning("Message {MessageId} expired before delivery", key); - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during acknowledgment cleanup"); - } - } - - private void RemoveMessageIdFromConnection(string connectionId, string messageId) - { - if (_connectionMessageIds.TryGetValue(connectionId, out var bag)) - { - var newBag = new ConcurrentBag(bag.Where(id => id != messageId)); - _connectionMessageIds.TryUpdate(connectionId, newBag, bag); - } - } - - public void Dispose() - { - _cleanupTimer?.Dispose(); - foreach (var pending in _pendingAcknowledgments.Values) - { - pending.TimeoutTokenSource?.Dispose(); - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Services/SignalRConnectionMonitor.cs b/ConduitLLM.Http/Services/SignalRConnectionMonitor.cs deleted file mode 100644 index 155b91e3c..000000000 --- a/ConduitLLM.Http/Services/SignalRConnectionMonitor.cs +++ /dev/null @@ -1,402 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Http.Interfaces; - -using Microsoft.AspNetCore.SignalR; - -using SignalRConnectionInfo = ConduitLLM.Http.Models.ConnectionInfo; - -namespace ConduitLLM.Http.Services -{ - /// - /// Service that monitors SignalR connections - /// - public interface ISignalRConnectionMonitor - { - /// - /// Records a new connection - /// - Task OnConnectionAsync(string connectionId, string hubName, HubCallerContext context); - - /// - /// Records a disconnection - /// - Task OnDisconnectionAsync(string connectionId); - - /// - /// Records activity on a connection - /// - Task RecordActivityAsync(string connectionId); - - /// - /// Adds a connection to a group - /// - Task AddToGroupAsync(string connectionId, string groupName); - - /// - /// Removes a connection from a group - /// - Task RemoveFromGroupAsync(string connectionId, string groupName); - - /// - /// Gets information about a specific connection - /// - ConduitLLM.Http.Models.ConnectionInfo? GetConnection(string connectionId); - - /// - /// Gets all active connections - /// - IEnumerable GetActiveConnections(); - - /// - /// Gets connections for a specific hub - /// - IEnumerable GetHubConnections(string hubName); - - /// - /// Gets connections for a specific virtual key - /// - IEnumerable GetVirtualKeyConnections(int virtualKeyId); - - /// - /// Gets connections in a specific group - /// - IEnumerable GetGroupConnections(string groupName); - - /// - /// Gets monitoring statistics - /// - ConnectionStatistics GetStatistics(); - - /// - /// Records a message sent to a connection - /// - Task RecordMessageSentAsync(string connectionId); - - /// - /// Records a message acknowledged by a connection - /// - Task RecordMessageAcknowledgedAsync(string connectionId); - } - - /// - /// Statistics about SignalR connections - /// - public class ConnectionStatistics - { - public int TotalActiveConnections { get; set; } - public Dictionary ConnectionsByHub { get; set; } = new(); - public Dictionary ConnectionsByTransport { get; set; } = new(); - public int TotalGroups { get; set; } - public int StaleConnections { get; set; } - public double AverageConnectionDurationMinutes { get; set; } - public double AverageIdleTimeMinutes { get; set; } - public DateTime OldestConnectionTime { get; set; } - public DateTime NewestConnectionTime { get; set; } - public long TotalMessagesSent { get; set; } - public long TotalMessagesAcknowledged { get; set; } - public double AcknowledgmentRate { get; set; } - } - - /// - /// Implementation of SignalR connection monitor - /// - public class SignalRConnectionMonitor : ISignalRConnectionMonitor, IHostedService, IDisposable - { - private readonly ILogger _logger; - private readonly IConfiguration _configuration; - private readonly ConcurrentDictionary _connections = new(); - private readonly ConcurrentDictionary> _groupConnections = new(); - private Timer? _cleanupTimer; - private readonly TimeSpan _staleConnectionThreshold; - private readonly TimeSpan _cleanupInterval; - - public SignalRConnectionMonitor( - ILogger logger, - IConfiguration configuration) - { - _logger = logger; - _configuration = configuration; - - _staleConnectionThreshold = TimeSpan.FromMinutes( - configuration.GetValue("SignalR:ConnectionMonitor:StaleThresholdMinutes", 60)); - _cleanupInterval = TimeSpan.FromMinutes( - configuration.GetValue("SignalR:ConnectionMonitor:CleanupIntervalMinutes", 5)); - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("SignalR Connection Monitor starting"); - - _cleanupTimer = new Timer( - CleanupStaleConnections, - null, - _cleanupInterval, - _cleanupInterval); - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("SignalR Connection Monitor stopping"); - - _cleanupTimer?.Change(Timeout.Infinite, 0); - - return Task.CompletedTask; - } - - public Task OnConnectionAsync(string connectionId, string hubName, HubCallerContext context) - { - var connectionInfo = new SignalRConnectionInfo - { - ConnectionId = connectionId, - HubName = hubName, - ConnectedAt = DateTime.UtcNow, - LastActivityAt = DateTime.UtcNow, - UserAgent = context.GetHttpContext()?.Request.Headers["User-Agent"].ToString(), - IpAddress = context.GetHttpContext()?.Connection.RemoteIpAddress?.ToString(), - TransportType = context.Features.Get()?.TransportType.ToString() - }; - - // Extract virtual key ID from context - if (context.Items.TryGetValue("VirtualKeyId", out var virtualKeyIdObj) && - virtualKeyIdObj is int virtualKeyId) - { - connectionInfo.VirtualKeyId = virtualKeyId; - } - - _connections.TryAdd(connectionId, connectionInfo); - - _logger.LogDebug( - "Connection {ConnectionId} established on {HubName} from {IpAddress} using {Transport}", - connectionId, hubName, connectionInfo.IpAddress, connectionInfo.TransportType); - - return Task.CompletedTask; - } - - public Task OnDisconnectionAsync(string connectionId) - { - if (_connections.TryRemove(connectionId, out var connectionInfo)) - { - // Remove from all groups - foreach (var kvp in _groupConnections) - { - kvp.Value.Remove(connectionId); - } - - _logger.LogDebug( - "Connection {ConnectionId} disconnected after {Duration}min with {MessagesSent} messages sent, {MessagesAcked} acknowledged", - connectionId, - connectionInfo.ConnectionDuration.TotalMinutes, - connectionInfo.MessagesSent, - connectionInfo.MessagesAcknowledged); - } - - return Task.CompletedTask; - } - - public Task RecordActivityAsync(string connectionId) - { - if (_connections.TryGetValue(connectionId, out var connectionInfo)) - { - connectionInfo.LastActivityAt = DateTime.UtcNow; - } - - return Task.CompletedTask; - } - - public Task AddToGroupAsync(string connectionId, string groupName) - { - if (_connections.TryGetValue(connectionId, out var connectionInfo)) - { - connectionInfo.Groups.Add(groupName); - - _groupConnections.AddOrUpdate( - groupName, - new HashSet { connectionId }, - (_, set) => { set.Add(connectionId); return set; }); - - _logger.LogDebug( - "Connection {ConnectionId} added to group {GroupName}", - connectionId, groupName); - } - - return Task.CompletedTask; - } - - public Task RemoveFromGroupAsync(string connectionId, string groupName) - { - if (_connections.TryGetValue(connectionId, out var connectionInfo)) - { - connectionInfo.Groups.Remove(groupName); - - if (_groupConnections.TryGetValue(groupName, out var connections)) - { - connections.Remove(connectionId); - } - - _logger.LogDebug( - "Connection {ConnectionId} removed from group {GroupName}", - connectionId, groupName); - } - - return Task.CompletedTask; - } - - public Task RecordMessageSentAsync(string connectionId) - { - if (_connections.TryGetValue(connectionId, out var connectionInfo)) - { - connectionInfo.MessagesSent++; - connectionInfo.LastActivityAt = DateTime.UtcNow; - } - - return Task.CompletedTask; - } - - public Task RecordMessageAcknowledgedAsync(string connectionId) - { - if (_connections.TryGetValue(connectionId, out var connectionInfo)) - { - connectionInfo.MessagesAcknowledged++; - connectionInfo.LastActivityAt = DateTime.UtcNow; - } - - return Task.CompletedTask; - } - - public SignalRConnectionInfo? GetConnection(string connectionId) - { - return _connections.TryGetValue(connectionId, out var connectionInfo) ? connectionInfo : null; - } - - public IEnumerable GetActiveConnections() - { - return _connections.Values.Where(c => !c.IsStale(_staleConnectionThreshold)); - } - - public IEnumerable GetHubConnections(string hubName) - { - return _connections.Values.Where(c => c.HubName == hubName); - } - - public IEnumerable GetVirtualKeyConnections(int virtualKeyId) - { - return _connections.Values.Where(c => c.VirtualKeyId == virtualKeyId); - } - - public IEnumerable GetGroupConnections(string groupName) - { - if (_groupConnections.TryGetValue(groupName, out var connectionIds)) - { - return connectionIds - .Select(id => _connections.TryGetValue(id, out var conn) ? conn : null) - .Where(c => c != null) - .Cast(); - } - - return Enumerable.Empty(); - } - - public ConnectionStatistics GetStatistics() - { - var allConnections = _connections.Values.ToList(); - var activeConnections = allConnections.Where(c => !c.IsStale(_staleConnectionThreshold)).ToList(); - - var stats = new ConnectionStatistics - { - TotalActiveConnections = activeConnections.Count, - StaleConnections = allConnections.Count - activeConnections.Count, - TotalGroups = _groupConnections.Count, - TotalMessagesSent = allConnections.Sum(c => c.MessagesSent), - TotalMessagesAcknowledged = allConnections.Sum(c => c.MessagesAcknowledged) - }; - - // Connections by hub - stats.ConnectionsByHub = activeConnections - .GroupBy(c => c.HubName) - .ToDictionary(g => g.Key, g => g.Count()); - - // Connections by transport - stats.ConnectionsByTransport = activeConnections - .Where(c => c.TransportType != null) - .GroupBy(c => c.TransportType!) - .ToDictionary(g => g.Key, g => g.Count()); - - if (activeConnections.Count() > 0) - { - stats.AverageConnectionDurationMinutes = activeConnections - .Average(c => c.ConnectionDuration.TotalMinutes); - stats.AverageIdleTimeMinutes = activeConnections - .Average(c => c.IdleTime.TotalMinutes); - stats.OldestConnectionTime = activeConnections - .Min(c => c.ConnectedAt); - stats.NewestConnectionTime = activeConnections - .Max(c => c.ConnectedAt); - } - - if (stats.TotalMessagesSent > 0) - { - stats.AcknowledgmentRate = (double)stats.TotalMessagesAcknowledged / stats.TotalMessagesSent * 100; - } - - return stats; - } - - private void CleanupStaleConnections(object? state) - { - try - { - var staleConnections = _connections.Values - .Where(c => c.IsStale(_staleConnectionThreshold)) - .ToList(); - - foreach (var connection in staleConnections) - { - if (_connections.TryRemove(connection.ConnectionId, out _)) - { - // Remove from all groups - foreach (var kvp in _groupConnections) - { - kvp.Value.Remove(connection.ConnectionId); - } - - _logger.LogWarning( - "Cleaned up stale connection {ConnectionId} from {HubName} (idle for {IdleMinutes}min)", - connection.ConnectionId, - connection.HubName, - connection.IdleTime.TotalMinutes); - } - } - - // Clean up empty groups - var emptyGroups = _groupConnections - .Where(kvp => kvp.Value.Count == 0) - .Select(kvp => kvp.Key) - .ToList(); - - foreach (var group in emptyGroups) - { - _groupConnections.TryRemove(group, out _); - } - - if (staleConnections.Count() > 0) - { - _logger.LogInformation( - "Cleaned up {Count} stale connections and {GroupCount} empty groups", - staleConnections.Count, emptyGroups.Count); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during stale connection cleanup"); - } - } - - public void Dispose() - { - _cleanupTimer?.Dispose(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Services/SignalRMessageBatcher.cs b/ConduitLLM.Http/Services/SignalRMessageBatcher.cs deleted file mode 100644 index f321aadd4..000000000 --- a/ConduitLLM.Http/Services/SignalRMessageBatcher.cs +++ /dev/null @@ -1,528 +0,0 @@ -using System.Collections.Concurrent; -using System.Text.Json; - -using ConduitLLM.Http.Models; - -using Microsoft.AspNetCore.SignalR; - -namespace ConduitLLM.Http.Services -{ - /// - /// Service that batches SignalR messages to improve performance - /// - public interface ISignalRMessageBatcher - { - /// - /// Adds a message to the batch queue - /// - Task AddMessageAsync(string hubName, string methodName, object message, string? connectionId = null, string? groupName = null, int priority = 0); - - /// - /// Gets current batching statistics - /// - BatchingStatistics GetStatistics(); - - /// - /// Forces immediate sending of all pending batches - /// - Task FlushAllBatchesAsync(); - - /// - /// Pauses batching (messages sent immediately) - /// - void PauseBatching(); - - /// - /// Resumes batching - /// - void ResumeBatching(); - } - - /// - /// Statistics about message batching - /// - public class BatchingStatistics - { - public long TotalMessagesBatched { get; set; } - public long TotalBatchesSent { get; set; } - public double AverageMessagesPerBatch { get; set; } - public long CurrentPendingMessages { get; set; } - public DateTime LastBatchSentAt { get; set; } - public TimeSpan AverageBatchLatency { get; set; } - public long NetworkCallsSaved { get; set; } - public bool IsBatchingEnabled { get; set; } - public Dictionary MessagesByMethod { get; set; } = new(); - public double BatchEfficiencyPercentage { get; set; } - } - - /// - /// Implementation of SignalR message batcher - /// - public class SignalRMessageBatcher : ISignalRMessageBatcher, IHostedService, IDisposable - { - private readonly ILogger _logger; - private readonly IConfiguration _configuration; - private readonly IServiceProvider _serviceProvider; - - // Batching data structures - private readonly ConcurrentDictionary _activeBatches = new(); - private readonly ConcurrentQueue _batchQueue = new(); - private readonly SemaphoreSlim _batchProcessingLock; - - // Timers - private Timer? _batchTimer; - private readonly object _timerLock = new(); - - // Configuration - private readonly TimeSpan _batchWindow; - private readonly int _maxBatchSize; - private readonly long _maxBatchSizeBytes; - private readonly bool _groupByMethod; - - // Statistics - private long _totalMessagesBatched; - private long _totalBatchesSent; - private long _totalBatchLatency; - private DateTime _lastBatchSentAt = DateTime.UtcNow; - private readonly ConcurrentDictionary _messagesByMethod = new(); - - // State - private bool _isBatchingEnabled = true; - private readonly object _stateLock = new(); - - public SignalRMessageBatcher( - ILogger logger, - IConfiguration configuration, - IServiceProvider serviceProvider) - { - _logger = logger; - _configuration = configuration; - _serviceProvider = serviceProvider; - - // Load configuration - _batchWindow = TimeSpan.FromMilliseconds(configuration.GetValue("SignalR:Batching:WindowMs", 100)); - _maxBatchSize = configuration.GetValue("SignalR:Batching:MaxBatchSize", 50); - _maxBatchSizeBytes = configuration.GetValue("SignalR:Batching:MaxBatchSizeBytes", 1024 * 1024); // 1MB default - _groupByMethod = configuration.GetValue("SignalR:Batching:GroupByMethod", true); - - _batchProcessingLock = new SemaphoreSlim(1, 1); - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _logger.LogInformation( - "SignalR Message Batcher starting with window: {Window}ms, max size: {MaxSize}", - _batchWindow.TotalMilliseconds, _maxBatchSize); - - _batchTimer = new Timer( - ProcessBatches, - null, - _batchWindow, - _batchWindow); - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("SignalR Message Batcher stopping"); - - lock (_timerLock) - { - _batchTimer?.Change(Timeout.Infinite, 0); - } - - // Flush remaining batches - FlushAllBatchesAsync().Wait(TimeSpan.FromSeconds(5)); - - return Task.CompletedTask; - } - - public async Task AddMessageAsync( - string hubName, - string methodName, - object message, - string? connectionId = null, - string? groupName = null, - int priority = 0) - { - if (!_isBatchingEnabled) - { - // Send immediately if batching is disabled - await SendMessageDirectlyAsync(hubName, methodName, message, connectionId, groupName); - return; - } - - var batchKey = new BatchKey(hubName, methodName, connectionId, groupName); - var messageSize = EstimateMessageSize(message); - - var batch = _activeBatches.AddOrUpdate( - batchKey.ToString(), - key => CreateNewBatch(batchKey), - (key, existingBatch) => existingBatch); - - lock (batch.SyncRoot) - { - // Check if adding this message would exceed limits - if (batch.Messages.Count() >= _maxBatchSize || - batch.TotalSizeBytes + messageSize > _maxBatchSizeBytes) - { - // Queue this batch for immediate sending - if (!batch.IsQueued) - { - batch.IsQueued = true; - _batchQueue.Enqueue(batchKey); - - // Trigger immediate processing - _ = Task.Run(async () => await ProcessBatchesAsync()); - } - - // Create a new batch for this message - batch = _activeBatches.AddOrUpdate( - batchKey.ToString(), - key => CreateNewBatch(batchKey), - (key, _) => CreateNewBatch(batchKey)); - } - - batch.Messages.Add(message); - batch.TotalSizeBytes += messageSize; - batch.Priority = Math.Max(batch.Priority, priority); - - if (message is SignalRMessage signalRMessage && signalRMessage.IsCritical) - { - batch.ContainsCriticalMessages = true; - } - - Interlocked.Increment(ref _totalMessagesBatched); - _messagesByMethod.AddOrUpdate(methodName, 1, (_, count) => count + 1); - } - - _logger.LogTrace( - "Added message to batch for {HubName}.{MethodName}, batch size: {Size}", - hubName, methodName, batch.Messages.Count()); - } - - public BatchingStatistics GetStatistics() - { - var stats = new BatchingStatistics - { - TotalMessagesBatched = _totalMessagesBatched, - TotalBatchesSent = _totalBatchesSent, - CurrentPendingMessages = _activeBatches.Sum(b => b.Value.Messages.Count()), - LastBatchSentAt = _lastBatchSentAt, - IsBatchingEnabled = _isBatchingEnabled, - MessagesByMethod = _messagesByMethod.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) - }; - - if (_totalBatchesSent > 0) - { - stats.AverageMessagesPerBatch = (double)_totalMessagesBatched / _totalBatchesSent; - stats.AverageBatchLatency = TimeSpan.FromMilliseconds(_totalBatchLatency / _totalBatchesSent); - stats.NetworkCallsSaved = _totalMessagesBatched - _totalBatchesSent; - stats.BatchEfficiencyPercentage = (1.0 - ((double)_totalBatchesSent / _totalMessagesBatched)) * 100; - } - - return stats; - } - - public async Task FlushAllBatchesAsync() - { - _logger.LogInformation("Flushing all pending batches"); - - var allBatchKeys = _activeBatches.Keys.ToList(); - foreach (var key in allBatchKeys) - { - if (_activeBatches.TryGetValue(key, out var batch)) - { - var batchKey = new BatchKey(batch.HubName, batch.MethodName, batch.ConnectionId, batch.GroupName); - await SendBatchAsync(batchKey, batch); - } - } - } - - public void PauseBatching() - { - lock (_stateLock) - { - _isBatchingEnabled = false; - _logger.LogInformation("Message batching paused"); - } - - // Flush pending batches - _ = Task.Run(async () => await FlushAllBatchesAsync()); - } - - public void ResumeBatching() - { - lock (_stateLock) - { - _isBatchingEnabled = true; - _logger.LogInformation("Message batching resumed"); - } - } - - private async void ProcessBatches(object? state) - { - await ProcessBatchesAsync(); - } - - private async Task ProcessBatchesAsync() - { - if (!await _batchProcessingLock.WaitAsync(0)) - { - // Already processing - return; - } - - try - { - var now = DateTime.UtcNow; - var batchesToSend = new List<(BatchKey Key, MessageBatch Batch)>(); - - // Check all active batches - foreach (var kvp in _activeBatches) - { - var batch = kvp.Value; - - lock (batch.SyncRoot) - { - if (batch.Messages.Count() > 0 && - (now - batch.CreatedAt >= _batchWindow || batch.IsQueued)) - { - var batchKey = new BatchKey(batch.HubName, batch.MethodName, batch.ConnectionId, batch.GroupName); - batchesToSend.Add((batchKey, batch)); - } - } - } - - // Send all ready batches - foreach (var (key, batch) in batchesToSend) - { - await SendBatchAsync(key, batch); - } - - // Process explicitly queued batches - while (_batchQueue.TryDequeue(out var batchKey)) - { - if (_activeBatches.TryGetValue(batchKey.ToString(), out var batch)) - { - await SendBatchAsync(batchKey, batch); - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error processing message batches"); - } - finally - { - _batchProcessingLock.Release(); - } - } - - private async Task SendBatchAsync(BatchKey batchKey, MessageBatch batch) - { - if (_activeBatches.TryRemove(batchKey.ToString(), out _)) - { - List messagesToSend; - int messageCount; - var batchLatency = DateTime.UtcNow - batch.CreatedAt; - - lock (batch.SyncRoot) - { - if (batch.Messages.Count() == 0) - { - return; - } - - messagesToSend = new List(batch.Messages); - messageCount = messagesToSend.Count(); - batch.Messages.Clear(); - } - - try - { - using var scope = _serviceProvider.CreateScope(); - var hubContext = GetHubContext(scope, batch.HubName); - - if (hubContext == null) - { - _logger.LogError("Could not find hub context for {HubName}", batch.HubName); - return; - } - - // Create batched message - var batchedMessage = new BatchedMessage - { - Messages = messagesToSend, - MethodName = batch.MethodName, - HubName = batch.HubName, - ConnectionId = batch.ConnectionId, - GroupName = batch.GroupName, - TotalSizeBytes = batch.TotalSizeBytes, - Priority = batch.Priority, - ContainsCriticalMessages = batch.ContainsCriticalMessages - }; - - // Send based on target - if (!string.IsNullOrEmpty(batch.ConnectionId)) - { - await hubContext.Clients.Client(batch.ConnectionId) - .SendAsync($"{batch.MethodName}Batch", batchedMessage); - } - else if (!string.IsNullOrEmpty(batch.GroupName)) - { - await hubContext.Clients.Group(batch.GroupName) - .SendAsync($"{batch.MethodName}Batch", batchedMessage); - } - else - { - await hubContext.Clients.All - .SendAsync($"{batch.MethodName}Batch", batchedMessage); - } - - Interlocked.Increment(ref _totalBatchesSent); - Interlocked.Add(ref _totalBatchLatency, (long)batchLatency.TotalMilliseconds); - _lastBatchSentAt = DateTime.UtcNow; - - _logger.LogDebug( - "Sent batch of {Count} messages for {HubName}.{MethodName}, latency: {Latency}ms", - messageCount, batch.HubName, batch.MethodName, batchLatency.TotalMilliseconds); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error sending batch for {HubName}.{MethodName}", - batch.HubName, batch.MethodName); - } - } - } - - private async Task SendMessageDirectlyAsync( - string hubName, - string methodName, - object message, - string? connectionId, - string? groupName) - { - try - { - using var scope = _serviceProvider.CreateScope(); - var hubContext = GetHubContext(scope, hubName); - - if (hubContext == null) - { - _logger.LogError("Could not find hub context for {HubName}", hubName); - return; - } - - if (!string.IsNullOrEmpty(connectionId)) - { - await hubContext.Clients.Client(connectionId).SendAsync(methodName, message); - } - else if (!string.IsNullOrEmpty(groupName)) - { - await hubContext.Clients.Group(groupName).SendAsync(methodName, message); - } - else - { - await hubContext.Clients.All.SendAsync(methodName, message); - } - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error sending message directly for {HubName}.{MethodName}", - hubName, methodName); - } - } - - private MessageBatch CreateNewBatch(BatchKey key) - { - return new MessageBatch - { - HubName = key.HubName, - MethodName = key.MethodName, - ConnectionId = key.ConnectionId, - GroupName = key.GroupName, - CreatedAt = DateTime.UtcNow - }; - } - - private long EstimateMessageSize(object message) - { - try - { - var json = JsonSerializer.Serialize(message); - return json.Length * sizeof(char); - } - catch - { - // Fallback estimate - return 1024; - } - } - - private IHubContext? GetHubContext(IServiceScope scope, string hubName) - { - var hubType = Type.GetType($"ConduitLLM.Http.Hubs.{hubName}, ConduitLLM.Http") ?? - Type.GetType($"ConduitLLM.Http.Hubs.{hubName}, ConduitLLM.Http"); - - if (hubType == null) - { - return null; - } - - var contextType = typeof(IHubContext<>).MakeGenericType(hubType); - return scope.ServiceProvider.GetService(contextType) as IHubContext; - } - - public void Dispose() - { - _batchTimer?.Dispose(); - _batchProcessingLock?.Dispose(); - } - - /// - /// Key for identifying unique batch targets - /// - private class BatchKey - { - public string HubName { get; } - public string MethodName { get; } - public string? ConnectionId { get; } - public string? GroupName { get; } - - public BatchKey(string hubName, string methodName, string? connectionId, string? groupName) - { - HubName = hubName; - MethodName = methodName; - ConnectionId = connectionId; - GroupName = groupName; - } - - public override string ToString() - { - return $"{HubName}:{MethodName}:{ConnectionId ?? "all"}:{GroupName ?? "none"}"; - } - } - - /// - /// Container for messages being batched - /// - private class MessageBatch - { - public List Messages { get; } = new(); - public string HubName { get; set; } = null!; - public string MethodName { get; set; } = null!; - public string? ConnectionId { get; set; } - public string? GroupName { get; set; } - public DateTime CreatedAt { get; set; } - public long TotalSizeBytes { get; set; } - public int Priority { get; set; } - public bool ContainsCriticalMessages { get; set; } - public bool IsQueued { get; set; } - public object SyncRoot { get; } = new(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Services/SignalRMessageQueueService.cs b/ConduitLLM.Http/Services/SignalRMessageQueueService.cs deleted file mode 100644 index 5beb202ae..000000000 --- a/ConduitLLM.Http/Services/SignalRMessageQueueService.cs +++ /dev/null @@ -1,464 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Http.Models; - -using Microsoft.AspNetCore.SignalR; - -using Polly; -using Polly.CircuitBreaker; - -namespace ConduitLLM.Http.Services -{ - /// - /// Service that manages queued messages for reliable SignalR delivery - /// - public interface ISignalRMessageQueueService - { - /// - /// Enqueues a message for delivery - /// - Task EnqueueMessageAsync(QueuedMessage message); - - /// - /// Gets current queue statistics - /// - QueueStatistics GetStatistics(); - - /// - /// Gets messages in the dead letter queue - /// - IEnumerable GetDeadLetterMessages(); - - /// - /// Requeues a dead letter message for retry - /// - Task RequeueDeadLetterAsync(string messageId); - } - - /// - /// Statistics about the message queue - /// - public class QueueStatistics - { - public int PendingMessages { get; set; } - public int DeadLetterMessages { get; set; } - public int ProcessedMessages { get; set; } - public int FailedMessages { get; set; } - public DateTime LastProcessedAt { get; set; } - public CircuitState CircuitBreakerState { get; set; } - public int ConsecutiveFailures { get; set; } - } - - /// - /// Implementation of SignalR message queue service - /// - public class SignalRMessageQueueService : ISignalRMessageQueueService, IHostedService, IDisposable - { - private readonly ILogger _logger; - private readonly IConfiguration _configuration; - private readonly IServiceProvider _serviceProvider; - private readonly ISignalRAcknowledgmentService _acknowledgmentService; - - private readonly ConcurrentQueue _messageQueue = new(); - private readonly ConcurrentBag _deadLetterQueue = new(); - private readonly SemaphoreSlim _processingLock; - - private Timer? _processingTimer; - private readonly IAsyncPolicy _retryPolicy; - private readonly IAsyncPolicy _circuitBreaker; - private CircuitState _currentCircuitState = CircuitState.Closed; - - // Configuration - private readonly int _maxRetryAttempts; - private readonly TimeSpan _initialRetryDelay; - private readonly TimeSpan _maxRetryDelay; - private readonly int _processingBatchSize; - private readonly TimeSpan _processingInterval; - private readonly int _circuitBreakerFailureThreshold; - private readonly TimeSpan _circuitBreakerDuration; - - // Statistics - private int _processedMessages; - private int _failedMessages; - private DateTime _lastProcessedAt = DateTime.UtcNow; - private int _consecutiveFailures; - - public SignalRMessageQueueService( - ILogger logger, - IConfiguration configuration, - IServiceProvider serviceProvider, - ISignalRAcknowledgmentService acknowledgmentService) - { - _logger = logger; - _configuration = configuration; - _serviceProvider = serviceProvider; - _acknowledgmentService = acknowledgmentService; - - // Load configuration - _maxRetryAttempts = configuration.GetValue("SignalR:MessageQueue:MaxRetryAttempts", 5); - _initialRetryDelay = TimeSpan.FromSeconds(configuration.GetValue("SignalR:MessageQueue:InitialRetryDelaySeconds", 2)); - _maxRetryDelay = TimeSpan.FromSeconds(configuration.GetValue("SignalR:MessageQueue:MaxRetryDelaySeconds", 32)); - _processingBatchSize = configuration.GetValue("SignalR:MessageQueue:ProcessingBatchSize", 100); - _processingInterval = TimeSpan.FromMilliseconds(configuration.GetValue("SignalR:MessageQueue:ProcessingIntervalMs", 100)); - _circuitBreakerFailureThreshold = configuration.GetValue("SignalR:MessageQueue:CircuitBreakerFailureThreshold", 5); - _circuitBreakerDuration = TimeSpan.FromSeconds(configuration.GetValue("SignalR:MessageQueue:CircuitBreakerDurationSeconds", 30)); - - _processingLock = new SemaphoreSlim(_processingBatchSize); - - // Configure retry policy with exponential backoff - _retryPolicy = Policy - .HandleResult(success => !success) - .WaitAndRetryAsync( - _maxRetryAttempts, - retryAttempt => TimeSpan.FromSeconds(Math.Min( - _initialRetryDelay.TotalSeconds * Math.Pow(2, retryAttempt - 1), - _maxRetryDelay.TotalSeconds)), - onRetry: (outcome, timespan, retryCount, context) => - { - var message = context.TryGetValue("message", out var msg) ? msg as QueuedMessage : null; - _logger.LogWarning( - "Retrying message {MessageId} delivery, attempt {RetryCount}/{MaxRetries}, delay: {Delay}ms", - message?.Message.MessageId, retryCount, _maxRetryAttempts, timespan.TotalMilliseconds); - }); - - // Configure circuit breaker - _circuitBreaker = Policy - .HandleResult(success => !success) - .CircuitBreakerAsync( - handledEventsAllowedBeforeBreaking: _circuitBreakerFailureThreshold, - durationOfBreak: _circuitBreakerDuration, - onBreak: (result, duration) => - { - _currentCircuitState = CircuitState.Open; - _logger.LogError( - "Circuit breaker opened due to {Failures} consecutive failures. Duration: {Duration}s", - _circuitBreakerFailureThreshold, duration.TotalSeconds); - }, - onReset: () => - { - _currentCircuitState = CircuitState.Closed; - _logger.LogInformation("Circuit breaker reset, resuming message processing"); - _consecutiveFailures = 0; - }, - onHalfOpen: () => - { - _currentCircuitState = CircuitState.HalfOpen; - _logger.LogInformation("Circuit breaker is half-open, testing message delivery"); - }); - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("SignalR Message Queue Service starting"); - - _processingTimer = new Timer( - ProcessMessages, - null, - _processingInterval, - _processingInterval); - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("SignalR Message Queue Service stopping"); - - _processingTimer?.Change(Timeout.Infinite, 0); - - // Wait for any in-flight processing to complete - try - { - _processingLock?.Wait(TimeSpan.FromSeconds(5)); - } - catch (ObjectDisposedException) - { - // Already disposed, ignore - } - - return Task.CompletedTask; - } - - public Task EnqueueMessageAsync(QueuedMessage message) - { - if (message.Message.IsExpired) - { - _logger.LogWarning("Attempted to enqueue expired message {MessageId}", message.Message.MessageId); - return Task.CompletedTask; - } - - _messageQueue.Enqueue(message); - _logger.LogDebug( - "Enqueued message {MessageId} for {HubName}.{MethodName}", - message.Message.MessageId, message.HubName, message.MethodName); - - return Task.CompletedTask; - } - - public QueueStatistics GetStatistics() - { - return new QueueStatistics - { - PendingMessages = _messageQueue.Count(), - DeadLetterMessages = _deadLetterQueue.Count(), - ProcessedMessages = _processedMessages, - FailedMessages = _failedMessages, - LastProcessedAt = _lastProcessedAt, - CircuitBreakerState = _currentCircuitState, - ConsecutiveFailures = _consecutiveFailures - }; - } - - public IEnumerable GetDeadLetterMessages() - { - return _deadLetterQueue.ToList(); - } - - public Task RequeueDeadLetterAsync(string messageId) - { - var message = _deadLetterQueue.FirstOrDefault(m => m.Message.MessageId == messageId); - if (message != null) - { - message.IsDeadLetter = false; - message.DeadLetterReason = null; - message.DeliveryAttempts = 0; - message.LastError = null; - message.NextDeliveryAt = DateTime.UtcNow; - - _messageQueue.Enqueue(message); - _logger.LogInformation("Requeued dead letter message {MessageId}", messageId); - } - - return Task.CompletedTask; - } - - private async void ProcessMessages(object? state) - { - if (_currentCircuitState == CircuitState.Open) - { - _logger.LogDebug("Circuit breaker is open, skipping message processing"); - return; - } - - var messagesToProcess = new List(); - var now = DateTime.UtcNow; - - // Dequeue messages that are ready for delivery - while (messagesToProcess.Count() < _processingBatchSize && _messageQueue.TryPeek(out var peekedMessage)) - { - if (peekedMessage.NextDeliveryAt <= now) - { - if (_messageQueue.TryDequeue(out var message)) - { - if (!message.Message.IsExpired) - { - messagesToProcess.Add(message); - } - else - { - _logger.LogWarning("Message {MessageId} expired, moving to dead letter", message.Message.MessageId); - MoveToDeadLetter(message, "Message expired"); - } - } - } - else - { - break; // No more messages ready for delivery - } - } - - if (messagesToProcess.Count() == 0) - { - return; - } - - _logger.LogDebug("Processing batch of {Count} messages", messagesToProcess.Count()); - - // Process messages in parallel with limited concurrency - var tasks = messagesToProcess.Select(async message => - { - await _processingLock.WaitAsync(); - try - { - var success = await ProcessSingleMessageAsync(message); - if (!success && message.DeliveryAttempts >= _maxRetryAttempts) - { - MoveToDeadLetter(message, $"Failed after {_maxRetryAttempts} attempts"); - } - else if (!success) - { - // Re-enqueue for retry - message.NextDeliveryAt = CalculateNextDeliveryTime(message.DeliveryAttempts); - _messageQueue.Enqueue(message); - } - } - finally - { - _processingLock.Release(); - } - }); - - await Task.WhenAll(tasks); - _lastProcessedAt = DateTime.UtcNow; - } - - private async Task ProcessSingleMessageAsync(QueuedMessage queuedMessage) - { - queuedMessage.DeliveryAttempts++; - queuedMessage.LastAttemptAt = DateTime.UtcNow; - - var context = new Context(); - context["message"] = queuedMessage; - - try - { - var result = await _circuitBreaker.ExecuteAsync(async (ctx) => - { - return await _retryPolicy.ExecuteAsync(async (retryCtx) => - { - return await DeliverMessageAsync(queuedMessage); - }, ctx); - }, context); - - if (result) - { - _processedMessages++; - _consecutiveFailures = 0; - _logger.LogDebug( - "Successfully delivered message {MessageId} after {Attempts} attempts", - queuedMessage.Message.MessageId, queuedMessage.DeliveryAttempts); - } - else - { - _failedMessages++; - _consecutiveFailures++; - } - - return result; - } - catch (BrokenCircuitException) - { - _logger.LogWarning( - "Circuit breaker is open, message {MessageId} delivery postponed", - queuedMessage.Message.MessageId); - queuedMessage.LastError = "Circuit breaker open"; - return false; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Unexpected error delivering message {MessageId}", - queuedMessage.Message.MessageId); - queuedMessage.LastError = ex.Message; - _failedMessages++; - _consecutiveFailures++; - return false; - } - } - - private async Task DeliverMessageAsync(QueuedMessage queuedMessage) - { - using var scope = _serviceProvider.CreateScope(); - var hubContext = GetHubContext(scope, queuedMessage.HubName); - - if (hubContext == null) - { - _logger.LogError("Could not find hub context for {HubName}", queuedMessage.HubName); - queuedMessage.LastError = $"Hub {queuedMessage.HubName} not found"; - return false; - } - - try - { - // Update retry count - queuedMessage.Message.RetryCount = queuedMessage.DeliveryAttempts - 1; - - // Send the message - if (!string.IsNullOrEmpty(queuedMessage.ConnectionId)) - { - // Direct message to specific connection - await hubContext.Clients.Client(queuedMessage.ConnectionId) - .SendAsync(queuedMessage.MethodName, queuedMessage.Message); - } - else if (!string.IsNullOrEmpty(queuedMessage.GroupName)) - { - // Message to group - await hubContext.Clients.Group(queuedMessage.GroupName) - .SendAsync(queuedMessage.MethodName, queuedMessage.Message); - } - else - { - _logger.LogError("Message {MessageId} has no target connection or group", - queuedMessage.Message.MessageId); - return false; - } - - // Register for acknowledgment if it's a critical message - if (queuedMessage.Message.IsCritical) - { - var pending = await _acknowledgmentService.RegisterMessageAsync( - queuedMessage.Message, - queuedMessage.ConnectionId ?? "group-message", - queuedMessage.HubName, - queuedMessage.MethodName, - queuedMessage.AcknowledgmentTimeout); - - // Wait for acknowledgment - var acknowledged = await pending.CompletionSource.Task; - return acknowledged; - } - - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error delivering message {MessageId} to {HubName}.{MethodName}", - queuedMessage.Message.MessageId, queuedMessage.HubName, queuedMessage.MethodName); - queuedMessage.LastError = ex.Message; - return false; - } - } - - private IHubContext? GetHubContext(IServiceScope scope, string hubName) - { - // This is a simplified version - in production, you'd want a more robust hub resolution mechanism - var hubType = Type.GetType($"ConduitLLM.Http.Hubs.{hubName}, ConduitLLM.Http") ?? - Type.GetType($"ConduitLLM.Http.Hubs.{hubName}, ConduitLLM.Http"); - - if (hubType == null) - { - return null; - } - - var contextType = typeof(IHubContext<>).MakeGenericType(hubType); - return scope.ServiceProvider.GetService(contextType) as IHubContext; - } - - private DateTime CalculateNextDeliveryTime(int attempts) - { - var delay = TimeSpan.FromSeconds(Math.Min( - _initialRetryDelay.TotalSeconds * Math.Pow(2, attempts), - _maxRetryDelay.TotalSeconds)); - - return DateTime.UtcNow.Add(delay); - } - - private void MoveToDeadLetter(QueuedMessage message, string reason) - { - message.IsDeadLetter = true; - message.DeadLetterReason = reason; - _deadLetterQueue.Add(message); - - _logger.LogWarning( - "Message {MessageId} moved to dead letter queue: {Reason}", - message.Message.MessageId, reason); - } - - public void Dispose() - { - _processingTimer?.Dispose(); - _processingLock?.Dispose(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Services/VirtualKeyRateLimitCache.cs b/ConduitLLM.Http/Services/VirtualKeyRateLimitCache.cs deleted file mode 100644 index 442003886..000000000 --- a/ConduitLLM.Http/Services/VirtualKeyRateLimitCache.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System.Collections.Concurrent; -using Microsoft.Extensions.Caching.Memory; -using ConduitLLM.Core.Interfaces; - -namespace ConduitLLM.Http.Services -{ - /// - /// Caches virtual key rate limit configurations for synchronous access - /// - public class VirtualKeyRateLimitCache : IHostedService - { - private readonly IServiceProvider _serviceProvider; - private readonly IMemoryCache _cache; - private readonly ILogger _logger; - private readonly ConcurrentDictionary _rateLimits; - private Timer? _refreshTimer; - - /// - /// Represents rate limit configuration for a virtual key - /// - public class VirtualKeyRateLimits - { - public int? RateLimitRpm { get; set; } - public int? RateLimitRpd { get; set; } - public DateTime LastUpdated { get; set; } - } - - /// - /// Initializes a new instance of VirtualKeyRateLimitCache - /// - public VirtualKeyRateLimitCache( - IServiceProvider serviceProvider, - IMemoryCache cache, - ILogger logger) - { - _serviceProvider = serviceProvider; - _cache = cache; - _logger = logger; - _rateLimits = new ConcurrentDictionary(); - } - - /// - /// Gets rate limits for a virtual key synchronously - /// - public VirtualKeyRateLimits? GetRateLimits(string virtualKeyHash) - { - if (_rateLimits.TryGetValue(virtualKeyHash, out var limits)) - { - // Check if cached value is still fresh (less than 1 minute old) - if (DateTime.UtcNow - limits.LastUpdated < TimeSpan.FromMinutes(1)) - { - return limits; - } - } - - // Try memory cache as backup - if (_cache.TryGetValue($"vkey_ratelimits:{virtualKeyHash}", out VirtualKeyRateLimits? cachedLimits) && cachedLimits != null) - { - _rateLimits.TryAdd(virtualKeyHash, cachedLimits); - return cachedLimits; - } - - return null; - } - - /// - /// Updates rate limits for a virtual key - /// - public void UpdateRateLimits(string virtualKeyHash, int? rpm, int? rpd) - { - var limits = new VirtualKeyRateLimits - { - RateLimitRpm = rpm, - RateLimitRpd = rpd, - LastUpdated = DateTime.UtcNow - }; - - _rateLimits.AddOrUpdate(virtualKeyHash, limits, (key, existing) => limits); - - // Also update memory cache with 5 minute expiration - _cache.Set($"vkey_ratelimits:{virtualKeyHash}", limits, TimeSpan.FromMinutes(5)); - } - - /// - /// Removes rate limits for a virtual key - /// - public void RemoveRateLimits(string virtualKeyHash) - { - _rateLimits.TryRemove(virtualKeyHash, out _); - _cache.Remove($"vkey_ratelimits:{virtualKeyHash}"); - } - - /// - /// Starts the background service - /// - public Task StartAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Virtual Key Rate Limit Cache service starting"); - - // Refresh rate limits every 30 seconds - _refreshTimer = new Timer(RefreshRateLimits, null, TimeSpan.Zero, TimeSpan.FromSeconds(30)); - - return Task.CompletedTask; - } - - /// - /// Stops the background service - /// - public Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Virtual Key Rate Limit Cache service stopping"); - - _refreshTimer?.Change(Timeout.Infinite, 0); - _refreshTimer?.Dispose(); - - return Task.CompletedTask; - } - - /// - /// Refreshes rate limits from the database - /// - private void RefreshRateLimits(object? state) - { - try - { - using var scope = _serviceProvider.CreateScope(); - var virtualKeyService = scope.ServiceProvider.GetRequiredService(); - - // Get all active virtual keys with rate limits - // This would require a new method in IVirtualKeyService to get all keys with rate limits - // For now, we'll update individual keys as they're accessed - - _logger.LogDebug("Virtual Key rate limits refresh completed"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing virtual key rate limits"); - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Services/WebhookDeliveryNotificationService.cs b/ConduitLLM.Http/Services/WebhookDeliveryNotificationService.cs deleted file mode 100644 index 4070e661b..000000000 --- a/ConduitLLM.Http/Services/WebhookDeliveryNotificationService.cs +++ /dev/null @@ -1,475 +0,0 @@ -using System.Collections.Concurrent; -using Microsoft.AspNetCore.SignalR; -using ConduitLLM.Configuration.DTOs.SignalR; -using ConduitLLM.Http.Hubs; - -namespace ConduitLLM.Http.Services -{ - /// - /// Service interface for sending webhook delivery notifications through SignalR. - /// - public interface IWebhookDeliveryNotificationService - { - /// - /// Notifies about a webhook delivery attempt. - /// - Task NotifyDeliveryAttemptAsync(string webhookUrl, string taskId, string taskType, string eventType, int attemptNumber); - - /// - /// Notifies about a successful webhook delivery. - /// - Task NotifyDeliverySuccessAsync(string webhookUrl, string taskId, int statusCode, long responseTimeMs, int totalAttempts); - - /// - /// Notifies about a failed webhook delivery. - /// - Task NotifyDeliveryFailureAsync(string webhookUrl, string taskId, string errorMessage, int? statusCode, int attemptNumber, bool isPermanent); - - /// - /// Notifies about a scheduled retry. - /// - Task NotifyRetryScheduledAsync(string webhookUrl, string taskId, DateTime retryTime, int retryNumber, int maxRetries); - - /// - /// Notifies about circuit breaker state change. - /// - Task NotifyCircuitBreakerStateChangeAsync(string webhookUrl, string newState, string previousState, string reason, int failureCount); - - /// - /// Gets current webhook delivery statistics. - /// - Task GetStatisticsAsync(string period = "last_hour"); - - /// - /// Records a delivery attempt for statistics. - /// - void RecordDeliveryAttempt(string webhookUrl); - - /// - /// Records a successful delivery for statistics. - /// - void RecordDeliverySuccess(string webhookUrl, long responseTimeMs); - - /// - /// Records a failed delivery for statistics. - /// - void RecordDeliveryFailure(string webhookUrl, bool isPermanent); - } - - /// - /// Implementation of webhook delivery notification service. - /// - public class WebhookDeliveryNotificationService : IWebhookDeliveryNotificationService, IHostedService - { - private readonly IHubContext _hubContext; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - - // Statistics tracking - private readonly ConcurrentDictionary _urlMetrics = new(); - private readonly ConcurrentQueue _recentEvents = new(); - private Timer? _statisticsTimer; - private const int MaxRecentEvents = 1000; - - public WebhookDeliveryNotificationService( - IHubContext hubContext, - IServiceProvider serviceProvider, - ILogger logger) - { - _hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext)); - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public Task StartAsync(CancellationToken cancellationToken) - { - // Start periodic statistics broadcasting - _statisticsTimer = new Timer( - async _ => await BroadcastStatisticsAsync(), - null, - TimeSpan.FromMinutes(1), - TimeSpan.FromMinutes(1)); - - _logger.LogInformation("WebhookDeliveryNotificationService started"); - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _statisticsTimer?.Dispose(); - _logger.LogInformation("WebhookDeliveryNotificationService stopped"); - return Task.CompletedTask; - } - - public async Task NotifyDeliveryAttemptAsync( - string webhookUrl, - string taskId, - string taskType, - string eventType, - int attemptNumber) - { - try - { - var attempt = new WebhookDeliveryAttempt - { - WebhookId = GenerateWebhookId(webhookUrl, taskId), - TaskId = taskId, - TaskType = taskType, - Url = webhookUrl, - EventType = eventType, - AttemptNumber = attemptNumber, - Timestamp = DateTime.UtcNow - }; - - // Get the hub directly to use broadcast method - using var scope = _serviceProvider.CreateScope(); - var hub = scope.ServiceProvider.GetService(); - if (hub != null) - { - await hub.BroadcastDeliveryAttempt(webhookUrl, attempt); - } - - RecordDeliveryAttempt(webhookUrl); - - _logger.LogDebug( - "Sent delivery attempt notification for {WebhookUrl}, attempt {AttemptNumber}", - webhookUrl, attemptNumber); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error sending delivery attempt notification"); - } - } - - public async Task NotifyDeliverySuccessAsync( - string webhookUrl, - string taskId, - int statusCode, - long responseTimeMs, - int totalAttempts) - { - try - { - var success = new WebhookDeliverySuccess - { - WebhookId = GenerateWebhookId(webhookUrl, taskId), - TaskId = taskId, - Url = webhookUrl, - StatusCode = statusCode, - ResponseTimeMs = responseTimeMs, - TotalAttempts = totalAttempts, - Timestamp = DateTime.UtcNow - }; - - // Broadcast to webhook-specific group - var groupName = GetWebhookGroupName(webhookUrl); - await _hubContext.Clients.Group(groupName).SendAsync("DeliverySucceeded", success); - - RecordDeliverySuccess(webhookUrl, responseTimeMs); - - _logger.LogInformation( - "Sent delivery success notification for {WebhookUrl}, response time: {ResponseTime}ms", - webhookUrl, responseTimeMs); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error sending delivery success notification"); - } - } - - public async Task NotifyDeliveryFailureAsync( - string webhookUrl, - string taskId, - string errorMessage, - int? statusCode, - int attemptNumber, - bool isPermanent) - { - try - { - var failure = new WebhookDeliveryFailure - { - WebhookId = GenerateWebhookId(webhookUrl, taskId), - TaskId = taskId, - Url = webhookUrl, - ErrorMessage = errorMessage, - StatusCode = statusCode, - AttemptNumber = attemptNumber, - IsPermanentFailure = isPermanent, - Timestamp = DateTime.UtcNow - }; - - // Broadcast to webhook-specific group - var groupName = GetWebhookGroupName(webhookUrl); - await _hubContext.Clients.Group(groupName).SendAsync("DeliveryFailed", failure); - - RecordDeliveryFailure(webhookUrl, isPermanent); - - _logger.LogWarning( - "Sent delivery failure notification for {WebhookUrl}, attempt {AttemptNumber}, permanent: {IsPermanent}", - webhookUrl, attemptNumber, isPermanent); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error sending delivery failure notification"); - } - } - - public async Task NotifyRetryScheduledAsync( - string webhookUrl, - string taskId, - DateTime retryTime, - int retryNumber, - int maxRetries) - { - try - { - var retry = new WebhookRetryInfo - { - WebhookId = GenerateWebhookId(webhookUrl, taskId), - DeliveryId = $"{taskId}-retry-{retryNumber}", - Url = webhookUrl, - EventType = "webhook.delivery", - ScheduledAt = retryTime, - NextAttemptNumber = retryNumber, - DelaySeconds = (retryTime - DateTime.UtcNow).TotalSeconds, - Reason = $"Retry {retryNumber} of {maxRetries}" - }; - - // Broadcast to webhook-specific group - var groupName = GetWebhookGroupName(webhookUrl); - await _hubContext.Clients.Group(groupName).SendAsync("RetryScheduled", retry); - - _logger.LogInformation( - "Sent retry scheduled notification for {WebhookUrl}, retry {RetryNumber}/{MaxRetries} at {RetryTime}", - webhookUrl, retryNumber, maxRetries, retryTime); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error sending retry scheduled notification"); - } - } - - public async Task NotifyCircuitBreakerStateChangeAsync( - string webhookUrl, - string newState, - string previousState, - string reason, - int failureCount) - { - try - { - var stateChange = new WebhookCircuitBreakerState - { - Url = webhookUrl, - CurrentState = newState, - PreviousState = previousState, - Reason = reason, - FailureCount = failureCount, - SuccessCount = 0, - Timestamp = DateTime.UtcNow - }; - - // Broadcast to webhook-specific group and all clients - var groupName = GetWebhookGroupName(webhookUrl); - await _hubContext.Clients.Group(groupName).SendAsync("CircuitBreakerStateChanged", stateChange); - await _hubContext.Clients.All.SendAsync("CircuitBreakerStateChanged", stateChange); - - _logger.LogWarning( - "Circuit breaker state changed for {WebhookUrl}: {PreviousState} -> {NewState}, reason: {Reason}", - webhookUrl, previousState, newState, reason); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error sending circuit breaker state change notification"); - } - } - - public void RecordDeliveryAttempt(string webhookUrl) - { - var metrics = _urlMetrics.GetOrAdd(webhookUrl, _ => new WebhookUrlMetrics()); - Interlocked.Increment(ref metrics.TotalAttempts); - - RecordEvent(new DeliveryEvent - { - Url = webhookUrl, - Type = DeliveryEventType.Attempt, - Timestamp = DateTime.UtcNow - }); - } - - public void RecordDeliverySuccess(string webhookUrl, long responseTimeMs) - { - var metrics = _urlMetrics.GetOrAdd(webhookUrl, _ => new WebhookUrlMetrics()); - Interlocked.Increment(ref metrics.Successes); - metrics.RecordResponseTime(responseTimeMs); - - RecordEvent(new DeliveryEvent - { - Url = webhookUrl, - Type = DeliveryEventType.Success, - ResponseTimeMs = responseTimeMs, - Timestamp = DateTime.UtcNow - }); - } - - public void RecordDeliveryFailure(string webhookUrl, bool isPermanent) - { - var metrics = _urlMetrics.GetOrAdd(webhookUrl, _ => new WebhookUrlMetrics()); - Interlocked.Increment(ref metrics.Failures); - if (!isPermanent) - { - Interlocked.Increment(ref metrics.PendingRetries); - } - - RecordEvent(new DeliveryEvent - { - Url = webhookUrl, - Type = DeliveryEventType.Failure, - IsPermanent = isPermanent, - Timestamp = DateTime.UtcNow - }); - } - - public async Task GetStatisticsAsync(string period = "last_hour") - { - var stats = new WebhookStatistics - { - Period = period, - UrlStatistics = new List() - }; - - // Calculate cutoff time based on period - var cutoffTime = period switch - { - "last_hour" => DateTime.UtcNow.AddHours(-1), - "last_day" => DateTime.UtcNow.AddDays(-1), - "last_week" => DateTime.UtcNow.AddDays(-7), - _ => DateTime.UtcNow.AddHours(-1) - }; - - // Get recent events within the period - var recentEvents = _recentEvents.Where(e => e.Timestamp >= cutoffTime).ToList(); - - // Calculate overall statistics - foreach (var urlMetrics in _urlMetrics) - { - var urlStats = new WebhookUrlStatistics - { - Url = urlMetrics.Key, - TotalDeliveries = urlMetrics.Value.TotalAttempts, - SuccessfulDeliveries = urlMetrics.Value.Successes, - FailedDeliveries = urlMetrics.Value.Failures, - AverageResponseTimeMs = urlMetrics.Value.GetAverageResponseTime(), - SuccessRate = urlMetrics.Value.TotalAttempts > 0 ? - (double)urlMetrics.Value.Successes / urlMetrics.Value.TotalAttempts * 100 : 0, - IsHealthy = true - }; - - stats.UrlStatistics.Add(urlStats); - - stats.TotalDeliveries += urlStats.TotalDeliveries; - stats.SuccessfulDeliveries += urlStats.SuccessfulDeliveries; - stats.FailedDeliveries += urlStats.FailedDeliveries; - } - - stats.PendingDeliveries = _urlMetrics.Values.Sum(m => m.PendingRetries); - stats.SuccessRate = stats.TotalDeliveries > 0 - ? (double)stats.SuccessfulDeliveries / stats.TotalDeliveries * 100 - : 0; - - // Calculate average response time from recent successful events - var successfulEvents = recentEvents - .Where(e => e.Type == DeliveryEventType.Success && e.ResponseTimeMs.HasValue) - .ToList(); - - stats.AverageResponseTimeMs = successfulEvents.Count() > 0 - ? successfulEvents.Average(e => e.ResponseTimeMs!.Value) - : 0; - - return await Task.FromResult(stats); - } - - private async Task BroadcastStatisticsAsync() - { - try - { - var stats = await GetStatisticsAsync(); - await _hubContext.Clients.All.SendAsync("DeliveryStatisticsUpdated", stats); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error broadcasting webhook statistics"); - } - } - - private void RecordEvent(DeliveryEvent evt) - { - _recentEvents.Enqueue(evt); - - // Keep queue size limited - while (_recentEvents.Count > MaxRecentEvents && _recentEvents.TryDequeue(out _)) - { - // Remove oldest events - } - } - - private static string GenerateWebhookId(string webhookUrl, string taskId) - { - return $"{taskId}_{webhookUrl.GetHashCode():X8}"; - } - - private static string GetWebhookGroupName(string webhookUrl) - { - var uri = new Uri(webhookUrl); - return $"webhook-{uri.Host.Replace(".", "-")}-{uri.AbsolutePath.Replace("/", "-")}"; - } - - /// - /// Internal class for tracking metrics per URL. - /// - private class WebhookUrlMetrics - { - public int TotalAttempts; - public int Successes; - public int Failures; - public int PendingRetries; - private readonly ConcurrentQueue _responseTimes = new(); - private const int MaxResponseTimes = 100; - - public void RecordResponseTime(long responseTimeMs) - { - _responseTimes.Enqueue(responseTimeMs); - while (_responseTimes.Count > MaxResponseTimes && _responseTimes.TryDequeue(out _)) - { - // Keep queue size limited - } - } - - public double GetAverageResponseTime() - { - var times = _responseTimes.ToArray(); - return times.Length > 0 ? times.Average() : 0; - } - } - - /// - /// Internal class for tracking delivery events. - /// - private class DeliveryEvent - { - public string Url { get; set; } = string.Empty; - public DeliveryEventType Type { get; set; } - public DateTime Timestamp { get; set; } - public long? ResponseTimeMs { get; set; } - public bool IsPermanent { get; set; } - } - - private enum DeliveryEventType - { - Attempt, - Success, - Failure - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/docs/SECURITY-ARCHITECTURE.md b/ConduitLLM.Http/docs/SECURITY-ARCHITECTURE.md deleted file mode 100644 index 3e9db8caa..000000000 --- a/ConduitLLM.Http/docs/SECURITY-ARCHITECTURE.md +++ /dev/null @@ -1,199 +0,0 @@ -# Core API Security Architecture - -## Overview - -The Core API has been enhanced with a comprehensive security architecture that provides multiple layers of protection including Virtual Key authentication, IP-based brute force protection, rate limiting, and security headers. - -## Key Security Features - -### 1. Virtual Key Authentication (URGENT FIX) - -Previously, the Core API endpoints were **completely unprotected**. The new architecture enforces Virtual Key authentication on all LLM endpoints. - -**Middleware**: `VirtualKeyAuthenticationMiddleware` -- Validates Virtual Keys from multiple headers (Authorization, api-key, X-API-Key, X-Virtual-Key) -- Checks if key is enabled and not expired -- Validates budget limits and model access -- Populates authentication context for downstream use - -**Supported Headers**: -``` -Authorization: Bearer condt_xxxx -api-key: condt_xxxx -X-API-Key: condt_xxxx -X-Virtual-Key: condt_xxxx (legacy) -``` - -### 2. IP-Based Brute Force Protection - -**Key Innovation**: Tracks failed authentication attempts **per IP across ALL Virtual Keys** to prevent attackers from cycling through multiple keys. - -**Features**: -- Bans IP after 10 failed attempts (configurable) -- Ban duration: 30 minutes (configurable) -- Shared tracking with Admin API and WebUI via Redis -- Automatic clearing on successful authentication - -**Protection Against**: -- Single-source brute force attacks ✅ -- Key enumeration attacks ✅ -- Distributed attacks (requires Cloudflare/CDN) ⚠️ - -### 3. Rate Limiting - -**Two-Layer Approach**: - -1. **IP-Based Rate Limiting** - - Default: 1000 requests/minute per IP - - Prevents abuse from single sources - - Applied before Virtual Key validation - -2. **Virtual Key Rate Limiting** - - Enforces per-key RPM/RPD limits from database - - Tracks usage with sliding windows - - Returns appropriate headers (X-RateLimit-Limit, X-RateLimit-Remaining) - -### 4. IP Filtering - -**Database-Driven Rules**: -- Whitelist/Blacklist support with CIDR notation -- Cached for performance (5-minute TTL) -- Private IP auto-allow option - -**Environment-Based Rules**: -- Quick deployment filtering via environment variables -- Supports comma-separated IP/CIDR lists - -### 5. Security Headers - -**API-Optimized Headers**: -- `X-Content-Type-Options: nosniff` - Prevent MIME sniffing -- `Strict-Transport-Security` - HTTPS enforcement -- Removes server identification headers -- Custom headers support - -## Configuration - -### Environment Variables - -```bash -# IP Filtering -CONDUIT_CORE_IP_FILTERING_ENABLED=true -CONDUIT_CORE_IP_FILTER_MODE=permissive -CONDUIT_CORE_IP_FILTER_ALLOW_PRIVATE=true -CONDUIT_CORE_IP_FILTER_WHITELIST=192.168.1.0/24,10.0.0.0/8 -CONDUIT_CORE_IP_FILTER_BLACKLIST=192.168.1.100 - -# IP-Based Rate Limiting -CONDUIT_CORE_RATE_LIMITING_ENABLED=true -CONDUIT_CORE_RATE_LIMIT_MAX_REQUESTS=1000 -CONDUIT_CORE_RATE_LIMIT_WINDOW_SECONDS=60 - -# Failed Authentication Protection -CONDUIT_CORE_MAX_FAILED_AUTH_ATTEMPTS=10 -CONDUIT_CORE_AUTH_BAN_DURATION_MINUTES=30 -CONDUIT_CORE_TRACK_FAILED_AUTH_ACROSS_KEYS=true - -# Virtual Key Enforcement -CONDUIT_CORE_ENFORCE_VKEY_RATE_LIMITS=true -CONDUIT_CORE_ENFORCE_VKEY_BUDGETS=true -CONDUIT_CORE_ENFORCE_VKEY_MODELS=true - -# Distributed Tracking (shared with Admin/WebUI) -CONDUIT_SECURITY_USE_DISTRIBUTED_TRACKING=true -``` - -## Shared Security Tracking - -The Core API shares security data with Admin API and WebUI through Redis: - -### Redis Key Structure -``` -rate_limit:core-api:{ip} - IP rate limiting -failed_login:{ip} - Failed auth attempts (cross-service) -ban:{ip} - Banned IPs (cross-service) -vkey_rate:rpm:{keyId} - Virtual Key RPM tracking -vkey_rate:rpd:{keyId} - Virtual Key RPD tracking -``` - -### Shared Ban List -When an IP is banned by any service (Core, Admin, WebUI), all services respect the ban. - -## Request Flow - -1. **Security Headers** - Added to all responses -2. **Virtual Key Authentication** - Extract and validate key -3. **Security Checks** - IP bans, rate limits, IP filters -4. **Rate Limiter** - Apply Virtual Key specific limits -5. **Request Processing** - Handle the API request - -## Migration Impact - -### Breaking Changes -- **All endpoints now require authentication** (previously open) -- Virtual Key header is mandatory for LLM endpoints - -### Backward Compatibility -- Supports multiple header formats -- Legacy X-Virtual-Key header still works -- Existing Virtual Keys continue to function - -## Security Best Practices - -### For Single-Source Attacks -1. **Enable IP-based rate limiting** - Prevents high-volume attacks -2. **Configure failed auth limits** - 10-20 attempts recommended -3. **Monitor ban list** - Check Redis for banned IPs -4. **Use IP filtering** - Restrict to known IP ranges when possible - -### For Distributed Attacks -1. **Use Cloudflare/CDN** - Application-level protection has limits -2. **Enable Cloudflare features**: - - Rate limiting rules - - Bot fight mode - - Geographic restrictions - - DDoS protection -3. **Monitor usage patterns** - Identify anomalies early - -## Monitoring and Troubleshooting - -### Common HTTP Status Codes -- `401 Unauthorized` - Missing/invalid Virtual Key -- `403 Forbidden` - IP banned or filtered -- `429 Too Many Requests` - Rate limit exceeded - -### Debug Logging -```bash -# Enable debug logging -Logging__LogLevel__ConduitLLM.Http.Services.SecurityService=Debug -Logging__LogLevel__ConduitLLM.Http.Middleware=Debug -``` - -### Check Security Status -```bash -# View banned IPs -redis-cli KEYS "ban:*" - -# View failed login attempts -redis-cli KEYS "failed_login:*" - -# Check rate limit status -redis-cli GET "rate_limit:core-api:192.168.1.100" - -# Virtual Key rate limits -redis-cli KEYS "vkey_rate:*" -``` - -## Performance Considerations - -- Security checks add ~1-2ms latency -- Redis caching minimizes database queries -- IP filter rules cached for 5 minutes -- Virtual Key validation cached for 60 seconds - -## Future Enhancements - -1. **Anomaly Detection** - ML-based pattern recognition -2. **Geo-blocking** - Country-level restrictions -3. **Advanced Rate Limiting** - Tiered limits by key type -4. **Security Analytics** - Detailed attack dashboards \ No newline at end of file diff --git a/ConduitLLM.Http/openapi-core.json b/ConduitLLM.Http/openapi-core.json deleted file mode 100644 index 8fcca7a4c..000000000 --- a/ConduitLLM.Http/openapi-core.json +++ /dev/null @@ -1,5054 +0,0 @@ -{ - "openapi": "3.0.4", - "info": { - "title": "Conduit Core API", - "description": "OpenAI-compatible API for multi-provider LLM access", - "version": "v1" - }, - "paths": { - "/v1/audio/transcriptions": { - "post": { - "tags": [ - "Audio" - ], - "summary": "Transcribes audio into text.", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "required": [ - "file" - ], - "type": "object", - "properties": { - "file": { - "type": "string", - "description": "The audio file to transcribe.", - "format": "binary" - }, - "model": { - "type": "string", - "description": "The model to use for transcription (e.g., \"whisper-1\").", - "default": "whisper-1" - }, - "language": { - "type": "string", - "description": "The language of the input audio (ISO-639-1)." - }, - "prompt": { - "type": "string", - "description": "Optional text to guide the model's style." - }, - "response_format": { - "type": "string", - "description": "The format of the transcript output." - }, - "temperature": { - "maximum": 1, - "minimum": 0, - "type": "number", - "description": "Sampling temperature between 0 and 1.", - "format": "double" - }, - "timestamp_granularities": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The timestamp granularities to populate." - } - } - }, - "encoding": { - "file": { - "style": "form" - }, - "model": { - "style": "form" - }, - "language": { - "style": "form" - }, - "prompt": { - "style": "form" - }, - "response_format": { - "style": "form" - }, - "temperature": { - "style": "form" - }, - "timestamp_granularities": { - "style": "form" - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/AudioTranscriptionResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/AudioTranscriptionResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/AudioTranscriptionResponse" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "429": { - "description": "Too Many Requests", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/audio/speech": { - "post": { - "tags": [ - "Audio" - ], - "summary": "Generates audio from input text.", - "requestBody": { - "description": "The text-to-speech request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TextToSpeechRequestDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request", - "content": { - "audio/mpeg": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/opus": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/aac": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/flac": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/wav": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/pcm": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "audio/mpeg": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/opus": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/aac": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/flac": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/wav": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/pcm": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "429": { - "description": "Too Many Requests", - "content": { - "audio/mpeg": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/opus": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/aac": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/flac": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/wav": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/pcm": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/audio/translations": { - "post": { - "tags": [ - "Audio" - ], - "summary": "Translates audio into English text.", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "required": [ - "file" - ], - "type": "object", - "properties": { - "file": { - "type": "string", - "description": "The audio file to translate.", - "format": "binary" - }, - "model": { - "type": "string", - "description": "The model to use for translation.", - "default": "whisper-1" - }, - "prompt": { - "type": "string", - "description": "Optional text to guide the model's style." - }, - "response_format": { - "type": "string", - "description": "The format of the translation output." - }, - "temperature": { - "maximum": 1, - "minimum": 0, - "type": "number", - "description": "Sampling temperature between 0 and 1.", - "format": "double" - } - } - }, - "encoding": { - "file": { - "style": "form" - }, - "model": { - "style": "form" - }, - "prompt": { - "style": "form" - }, - "response_format": { - "style": "form" - }, - "temperature": { - "style": "form" - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/AudioTranscriptionResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/AudioTranscriptionResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/AudioTranscriptionResponse" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/auth/ephemeral-key": { - "post": { - "tags": [ - "Authentication" - ], - "summary": "Generate an ephemeral key for the authenticated virtual key", - "requestBody": { - "description": "Optional metadata for the ephemeral key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenerateEphemeralKeyRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/GenerateEphemeralKeyRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/GenerateEphemeralKeyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "Ephemeral key generated successfully", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/EphemeralKeyResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/EphemeralKeyResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/EphemeralKeyResponse" - } - } - } - }, - "401": { - "description": "Authentication failed", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/batch/spend-updates": { - "post": { - "tags": [ - "BatchOperations" - ], - "summary": "Start a batch spend update operation", - "requestBody": { - "description": "Batch spend update request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BatchSpendUpdateRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/BatchSpendUpdateRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/BatchSpendUpdateRequest" - } - } - } - }, - "responses": { - "202": { - "description": "Accepted", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/BatchOperationStartResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/BatchOperationStartResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/BatchOperationStartResponse" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/batch/virtual-key-updates": { - "post": { - "tags": [ - "BatchOperations" - ], - "summary": "Start a batch virtual key update operation", - "requestBody": { - "description": "Batch virtual key update request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BatchVirtualKeyUpdateRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/BatchVirtualKeyUpdateRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/BatchVirtualKeyUpdateRequest" - } - } - } - }, - "responses": { - "202": { - "description": "Accepted", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/BatchOperationStartResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/BatchOperationStartResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/BatchOperationStartResponse" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/batch/webhook-sends": { - "post": { - "tags": [ - "BatchOperations" - ], - "summary": "Start a batch webhook send operation", - "requestBody": { - "description": "Batch webhook send request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BatchWebhookSendRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/BatchWebhookSendRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/BatchWebhookSendRequest" - } - } - } - }, - "responses": { - "202": { - "description": "Accepted", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/BatchOperationStartResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/BatchOperationStartResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/BatchOperationStartResponse" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/batch/operations/{operationId}": { - "get": { - "tags": [ - "BatchOperations" - ], - "summary": "Get the status of a batch operation", - "parameters": [ - { - "name": "operationId", - "in": "path", - "description": "Operation ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/BatchOperationStatusResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/BatchOperationStatusResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/BatchOperationStatusResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/batch/operations/{operationId}/cancel": { - "post": { - "tags": [ - "BatchOperations" - ], - "summary": "Cancel an active batch operation", - "parameters": [ - { - "name": "operationId", - "in": "path", - "description": "Operation ID to cancel", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "No Content" - }, - "404": { - "description": "Not Found", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "409": { - "description": "Conflict", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/chat/completions": { - "post": { - "tags": [ - "Chat" - ], - "summary": "Creates a chat completion.", - "requestBody": { - "description": "The chat completion request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChatCompletionRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ChatCompletionRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ChatCompletionRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ChatCompletionResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChatCompletionResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ChatCompletionResponse" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - } - } - } - } - } - }, - "/v1/completions": { - "post": { - "tags": [ - "Completions" - ], - "summary": "Legacy completions endpoint - not implemented.", - "responses": { - "501": { - "description": "Not Implemented", - "content": { - "text/plain": { - "schema": { } - }, - "application/json": { - "schema": { } - }, - "text/json": { - "schema": { } - } - } - } - } - } - }, - "/v1/discovery/models": { - "get": { - "tags": [ - "Discovery" - ], - "summary": "Gets all discovered models and their capabilities for authenticated virtual keys.", - "parameters": [ - { - "name": "capability", - "in": "query", - "description": "Optional capability filter (e.g., \"video_generation\", \"vision\")", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/discovery/capabilities": { - "get": { - "tags": [ - "Discovery" - ], - "summary": "Gets all available capabilities in the system.", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/discovery/models/{model}/parameters": { - "get": { - "tags": [ - "Discovery" - ], - "summary": "Gets UI parameters for a specific model to enable dynamic UI generation.", - "description": "This endpoint returns the UI-focused parameter definitions from the ModelSeries.Parameters field,\nwhich contains JSON objects defining sliders, selects, textareas, and other UI controls.\nThis allows clients to dynamically generate appropriate UI controls without Admin API access.", - "parameters": [ - { - "name": "model", - "in": "path", - "description": "The model alias or identifier to get parameters for", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/downloads/{fileId}": { - "get": { - "tags": [ - "Downloads" - ], - "summary": "Downloads a file by its identifier with support for range requests.", - "parameters": [ - { - "name": "fileId", - "in": "path", - "description": "The file identifier (storage key or URL).", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "inline", - "in": "query", - "description": "Whether to display inline (true) or force download (false).", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "head": { - "tags": [ - "Downloads" - ], - "summary": "Checks if a file exists.", - "parameters": [ - { - "name": "fileId", - "in": "path", - "description": "The file identifier.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/downloads/metadata/{fileId}": { - "get": { - "tags": [ - "Downloads" - ], - "summary": "Gets metadata information about a file.", - "parameters": [ - { - "name": "fileId", - "in": "path", - "description": "The file identifier.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/downloads/generate-url": { - "post": { - "tags": [ - "Downloads" - ], - "summary": "Generates a temporary download URL for a file.", - "requestBody": { - "description": "The URL generation request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenerateUrlRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/GenerateUrlRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/GenerateUrlRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/embeddings": { - "post": { - "tags": [ - "Embeddings" - ], - "summary": "Creates embeddings for the given input.", - "requestBody": { - "description": "The embedding request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmbeddingRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/EmbeddingRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/EmbeddingRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/EmbeddingResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmbeddingResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/EmbeddingResponse" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - } - } - } - } - } - }, - "/api/test/health-monitoring/scenarios": { - "get": { - "tags": [ - "HealthMonitoringTest" - ], - "summary": "Get available test scenarios", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/test/health-monitoring/start/{scenario}": { - "post": { - "tags": [ - "HealthMonitoringTest" - ], - "summary": "Start a test scenario", - "parameters": [ - { - "name": "scenario", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "durationSeconds", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 60 - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/test/health-monitoring/stop/{scenario}": { - "post": { - "tags": [ - "HealthMonitoringTest" - ], - "summary": "Stop a running test scenario", - "parameters": [ - { - "name": "scenario", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/test/health-monitoring/active": { - "get": { - "tags": [ - "HealthMonitoringTest" - ], - "summary": "Get currently running scenarios", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/test/health-monitoring/alert": { - "post": { - "tags": [ - "HealthMonitoringTest" - ], - "summary": "Trigger a custom alert", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomAlertRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CustomAlertRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CustomAlertRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/audio/hybrid/process": { - "post": { - "tags": [ - "HybridAudio" - ], - "summary": "Processes audio input through the hybrid STT-LLM-TTS pipeline.", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "file": { - "type": "string", - "description": "The audio file to process.", - "format": "binary" - }, - "sessionId": { - "type": "string", - "description": "Optional session ID for maintaining conversation context." - }, - "language": { - "type": "string", - "description": "Optional language code for transcription." - }, - "systemPrompt": { - "type": "string", - "description": "Optional system prompt for the LLM." - }, - "voiceId": { - "type": "string", - "description": "Optional voice ID for TTS synthesis." - }, - "outputFormat": { - "type": "string", - "description": "Desired output audio format (default: mp3).", - "default": "mp3" - }, - "temperature": { - "type": "number", - "description": "Temperature for LLM response generation (0.0-2.0).", - "format": "double", - "default": 0.7 - }, - "maxTokens": { - "type": "integer", - "description": "Maximum tokens for the LLM response.", - "format": "int32", - "default": 150 - } - } - }, - "encoding": { - "file": { - "style": "form" - }, - "sessionId": { - "style": "form" - }, - "language": { - "style": "form" - }, - "systemPrompt": { - "style": "form" - }, - "voiceId": { - "style": "form" - }, - "outputFormat": { - "style": "form" - }, - "temperature": { - "style": "form" - }, - "maxTokens": { - "style": "form" - } - } - } - } - }, - "responses": { - "200": { - "description": "Returns the synthesized audio data.", - "content": { - "audio/mpeg": { - "schema": { - "type": "string", - "format": "binary" - } - }, - "audio/wav": { - "schema": { - "type": "string", - "format": "binary" - } - }, - "audio/flac": { - "schema": { - "type": "string", - "format": "binary" - } - } - } - }, - "400": { - "description": "If the request is invalid.", - "content": { - "audio/mpeg": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/wav": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/flac": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "401": { - "description": "If authentication fails.", - "content": { - "audio/mpeg": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/wav": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/flac": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "403": { - "description": "If the user lacks audio permissions.", - "content": { - "audio/mpeg": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/wav": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "audio/flac": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "500": { - "description": "If an internal error occurs." - } - } - } - }, - "/v1/audio/hybrid/sessions": { - "post": { - "tags": [ - "HybridAudio" - ], - "summary": "Creates a new conversation session for maintaining context.", - "requestBody": { - "description": "The session configuration.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HybridSessionConfig" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/HybridSessionConfig" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/HybridSessionConfig" - } - } - } - }, - "responses": { - "200": { - "description": "Returns the session ID.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSessionResponse" - } - } - } - }, - "400": { - "description": "If the configuration is invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "401": { - "description": "If authentication fails.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "403": { - "description": "If the user lacks audio permissions.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/audio/hybrid/sessions/{sessionId}": { - "delete": { - "tags": [ - "HybridAudio" - ], - "summary": "Closes an active conversation session.", - "parameters": [ - { - "name": "sessionId", - "in": "path", - "description": "The session ID to close.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "Session closed successfully." - }, - "400": { - "description": "If the session ID is invalid.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "401": { - "description": "If authentication fails.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/audio/hybrid/status": { - "get": { - "tags": [ - "HybridAudio" - ], - "summary": "Checks if the hybrid audio service is available.", - "responses": { - "200": { - "description": "Returns the availability status.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceStatus" - } - } - } - }, - "401": { - "description": "If authentication fails.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/images/generations/async": { - "post": { - "tags": [ - "Images" - ], - "summary": "Creates an async image generation task.", - "requestBody": { - "description": "The image generation request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ImageGenerationRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ImageGenerationRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ImageGenerationRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/images/generations/{taskId}/status": { - "get": { - "tags": [ - "Images" - ], - "summary": "Gets the status of an async image generation task.", - "parameters": [ - { - "name": "taskId", - "in": "path", - "description": "The task ID.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/images/generations/{taskId}": { - "delete": { - "tags": [ - "Images" - ], - "summary": "Cancels an async image generation task.", - "parameters": [ - { - "name": "taskId", - "in": "path", - "description": "The task ID to cancel.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/images/generations": { - "post": { - "tags": [ - "Images" - ], - "summary": "Creates one or more images given a prompt.", - "requestBody": { - "description": "The image generation request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ImageGenerationRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ImageGenerationRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ImageGenerationRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/media/{storageKey}": { - "get": { - "tags": [ - "Media" - ], - "summary": "Retrieves a media file by its storage key.", - "parameters": [ - { - "name": "storageKey", - "in": "path", - "description": "The unique storage key.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "head": { - "tags": [ - "Media" - ], - "summary": "Checks if a media file exists.", - "parameters": [ - { - "name": "storageKey", - "in": "path", - "description": "The unique storage key.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/media/info/{storageKey}": { - "get": { - "tags": [ - "Media" - ], - "summary": "Gets metadata information about a media file.", - "parameters": [ - { - "name": "storageKey", - "in": "path", - "description": "The unique storage key.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/models": { - "get": { - "tags": [ - "Models" - ], - "summary": "Lists available models.", - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { } - }, - "application/json": { - "schema": { } - }, - "text/json": { - "schema": { } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - } - } - } - } - } - }, - "/v1/models/{modelId}/metadata": { - "get": { - "tags": [ - "Models" - ], - "summary": "Gets metadata for a specific model.", - "parameters": [ - { - "name": "modelId", - "in": "path", - "description": "The model ID.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { } - }, - "application/json": { - "schema": { } - }, - "text/json": { - "schema": { } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/OpenAIErrorResponse" - } - } - } - } - } - } - }, - "/api/provider-models/{providerId}": { - "get": { - "tags": [ - "ProviderModels" - ], - "summary": "Gets models that are compatible with a specified provider based on provider type", - "parameters": [ - { - "name": "providerId", - "in": "path", - "description": "ID of the provider", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "text/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "text/plain": { - "schema": { } - }, - "application/json": { - "schema": { } - }, - "text/json": { - "schema": { } - } - } - } - } - } - }, - "/v1/realtime/connect": { - "get": { - "tags": [ - "Realtime" - ], - "summary": "Establishes a WebSocket connection for real-time audio streaming.", - "parameters": [ - { - "name": "model", - "in": "query", - "description": "The model to use for the real-time session (e.g., \"gpt-4o-realtime-preview\")", - "schema": { - "type": "string" - } - }, - { - "name": "provider", - "in": "query", - "description": "Optional provider override (defaults to routing based on model)", - "schema": { - "type": "string" - } - } - ], - "responses": { - "101": { - "description": "WebSocket connection established" - }, - "400": { - "description": "Invalid request or WebSocket not supported", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "401": { - "description": "Authentication failed", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "403": { - "description": "Virtual key does not have access to real-time features", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "503": { - "description": "No available providers for the requested model" - } - } - } - }, - "/v1/realtime/connections": { - "get": { - "tags": [ - "Realtime" - ], - "summary": "Gets the status of active real-time connections for the authenticated user.", - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ConnectionStatusResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConnectionStatusResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ConnectionStatusResponse" - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/realtime/connections/{connectionId}": { - "delete": { - "tags": [ - "Realtime" - ], - "summary": "Terminates a specific real-time connection.", - "parameters": [ - { - "name": "connectionId", - "in": "path", - "description": "The ID of the connection to terminate", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "No Content" - }, - "401": { - "description": "Unauthorized", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/api/signalr/batching/statistics": { - "get": { - "tags": [ - "SignalRBatching" - ], - "summary": "Gets current batching statistics", - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/BatchingStatistics" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/BatchingStatistics" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/BatchingStatistics" - } - } - } - } - } - } - }, - "/api/signalr/batching/pause": { - "post": { - "tags": [ - "SignalRBatching" - ], - "summary": "Pauses message batching (messages sent immediately)", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/signalr/batching/resume": { - "post": { - "tags": [ - "SignalRBatching" - ], - "summary": "Resumes message batching", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/signalr/batching/flush": { - "post": { - "tags": [ - "SignalRBatching" - ], - "summary": "Forces immediate sending of all pending batches", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/signalr/batching/efficiency": { - "get": { - "tags": [ - "SignalRBatching" - ], - "summary": "Gets batching efficiency metrics", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/health/signalr/connections": { - "get": { - "tags": [ - "SignalRHealth" - ], - "summary": "Gets SignalR connection statistics", - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ConnectionStatistics" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConnectionStatistics" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ConnectionStatistics" - } - } - } - } - } - } - }, - "/health/signalr/queue": { - "get": { - "tags": [ - "SignalRHealth" - ], - "summary": "Gets SignalR message queue statistics", - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/QueueStatistics" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/QueueStatistics" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/QueueStatistics" - } - } - } - } - } - } - }, - "/health/signalr/connections/details": { - "get": { - "tags": [ - "SignalRHealth" - ], - "summary": "Gets detailed connection information (requires admin auth)", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/health/signalr/connections/hub/{hubName}": { - "get": { - "tags": [ - "SignalRHealth" - ], - "summary": "Gets connections for a specific hub", - "parameters": [ - { - "name": "hubName", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/health/signalr/connections/key/{virtualKeyId}": { - "get": { - "tags": [ - "SignalRHealth" - ], - "summary": "Gets connections for a specific virtual key", - "parameters": [ - { - "name": "virtualKeyId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/health/signalr/connections/group/{groupName}": { - "get": { - "tags": [ - "SignalRHealth" - ], - "summary": "Gets connections in a specific group", - "parameters": [ - { - "name": "groupName", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/health/signalr/queue/deadletter": { - "get": { - "tags": [ - "SignalRHealth" - ], - "summary": "Gets dead letter queue messages", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/health/signalr/queue/deadletter/{messageId}/requeue": { - "post": { - "tags": [ - "SignalRHealth" - ], - "summary": "Requeues a dead letter message", - "parameters": [ - { - "name": "messageId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/health/signalr": { - "get": { - "tags": [ - "SignalRHealth" - ], - "summary": "Gets overall SignalR health status", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/tasks/{taskId}": { - "get": { - "tags": [ - "Tasks" - ], - "summary": "Gets the status of a specific task.", - "parameters": [ - { - "name": "taskId", - "in": "path", - "description": "The ID of the task to retrieve.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/tasks/{taskId}/cancel": { - "post": { - "tags": [ - "Tasks" - ], - "summary": "Cancels a running task.", - "parameters": [ - { - "name": "taskId", - "in": "path", - "description": "The ID of the task to cancel.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/tasks/{taskId}/poll": { - "get": { - "tags": [ - "Tasks" - ], - "summary": "Polls a task until it completes or times out.", - "parameters": [ - { - "name": "taskId", - "in": "path", - "description": "The ID of the task to poll.", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "timeout", - "in": "query", - "description": "Maximum time to wait in seconds (default: 300, max: 600).", - "schema": { - "type": "integer", - "format": "int32", - "default": 300 - } - }, - { - "name": "interval", - "in": "query", - "description": "Polling interval in seconds (default: 2, min: 1).", - "schema": { - "type": "integer", - "format": "int32", - "default": 2 - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/videos/generations/async": { - "post": { - "tags": [ - "Videos" - ], - "summary": "Starts an asynchronous video generation task.", - "requestBody": { - "description": "The video generation request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VideoGenerationRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/VideoGenerationRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/VideoGenerationRequest" - } - } - }, - "required": true - }, - "responses": { - "202": { - "description": "Video generation task started.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/VideoGenerationTaskResponse" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/VideoGenerationTaskResponse" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/VideoGenerationTaskResponse" - } - } - } - }, - "400": { - "description": "Invalid request parameters.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "401": { - "description": "Authentication failed.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "403": { - "description": "Virtual key does not have permission.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "429": { - "description": "Rate limit exceeded.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/videos/generations/tasks/{taskId}": { - "get": { - "tags": [ - "Videos" - ], - "summary": "Gets the status of a video generation task.", - "parameters": [ - { - "name": "taskId", - "in": "path", - "description": "The task ID returned from the async generation endpoint.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Task status retrieved successfully.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/VideoGenerationTaskStatus" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/VideoGenerationTaskStatus" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/VideoGenerationTaskStatus" - } - } - } - }, - "401": { - "description": "Authentication failed.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "404": { - "description": "Task not found or access denied.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/videos/generations/tasks/{taskId}/retry": { - "post": { - "tags": [ - "Videos" - ], - "summary": "Manually retries a failed video generation task.", - "parameters": [ - { - "name": "taskId", - "in": "path", - "description": "The task ID to retry.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Task queued for retry.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/VideoGenerationTaskStatus" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/VideoGenerationTaskStatus" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/VideoGenerationTaskStatus" - } - } - } - }, - "400": { - "description": "Task cannot be retried (not failed or exceeded max retries).", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "401": { - "description": "Authentication failed.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "404": { - "description": "Task not found or access denied.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/v1/videos/generations/{taskId}": { - "delete": { - "tags": [ - "Videos" - ], - "summary": "Cancels a video generation task.", - "parameters": [ - { - "name": "taskId", - "in": "path", - "description": "The task ID to cancel.", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "Task cancelled successfully." - }, - "401": { - "description": "Authentication failed.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "404": { - "description": "Task not found or access denied.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "409": { - "description": "Task cannot be cancelled (already completed or failed).", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "AlertSeverity": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "format": "int32" - }, - "AudioTranscriptionResponse": { - "type": "object", - "properties": { - "text": { - "type": "string", - "nullable": true - }, - "language": { - "type": "string", - "nullable": true - }, - "duration": { - "type": "number", - "format": "double", - "nullable": true - }, - "segments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TranscriptionSegment" - }, - "nullable": true - }, - "words": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TranscriptionWord" - }, - "nullable": true - }, - "alternatives": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TranscriptionAlternative" - }, - "nullable": true - }, - "confidence": { - "type": "number", - "format": "double", - "nullable": true - }, - "metadata": { - "type": "object", - "additionalProperties": { }, - "nullable": true - }, - "model": { - "type": "string", - "nullable": true - }, - "usage": { - "$ref": "#/components/schemas/AudioUsage" - } - }, - "additionalProperties": false - }, - "AudioUsage": { - "type": "object", - "properties": { - "audioSeconds": { - "type": "number", - "format": "double" - }, - "characterCount": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "additionalMetrics": { - "type": "object", - "additionalProperties": { }, - "nullable": true - } - }, - "additionalProperties": false - }, - "BatchOperationStartResponse": { - "type": "object", - "properties": { - "operationId": { - "type": "string", - "nullable": true - }, - "operationType": { - "type": "string", - "nullable": true - }, - "totalItems": { - "type": "integer", - "format": "int32" - }, - "statusUrl": { - "type": "string", - "nullable": true - }, - "taskId": { - "type": "string", - "nullable": true - }, - "signalREvents": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "message": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "BatchOperationStatusResponse": { - "type": "object", - "properties": { - "operationId": { - "type": "string", - "nullable": true - }, - "operationType": { - "type": "string", - "nullable": true - }, - "status": { - "type": "string", - "nullable": true - }, - "totalItems": { - "type": "integer", - "format": "int32" - }, - "processedCount": { - "type": "integer", - "format": "int32" - }, - "successCount": { - "type": "integer", - "format": "int32" - }, - "failedCount": { - "type": "integer", - "format": "int32" - }, - "progressPercentage": { - "type": "integer", - "format": "int32" - }, - "elapsedTime": { - "type": "string", - "format": "date-span" - }, - "estimatedTimeRemaining": { - "type": "string", - "format": "date-span" - }, - "itemsPerSecond": { - "type": "number", - "format": "double" - }, - "currentItem": { - "type": "string", - "nullable": true - }, - "canCancel": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "BatchSpendUpdateRequest": { - "required": [ - "updates" - ], - "type": "object", - "properties": { - "updates": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SpendUpdateDto" - } - } - }, - "additionalProperties": false - }, - "BatchVirtualKeyUpdateRequest": { - "required": [ - "updates" - ], - "type": "object", - "properties": { - "updates": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VirtualKeyUpdateDto" - } - } - }, - "additionalProperties": false - }, - "BatchWebhookSendRequest": { - "required": [ - "webhooks" - ], - "type": "object", - "properties": { - "webhooks": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WebhookSendDto" - } - } - }, - "additionalProperties": false - }, - "BatchingStatistics": { - "type": "object", - "properties": { - "totalMessagesBatched": { - "type": "integer", - "format": "int64" - }, - "totalBatchesSent": { - "type": "integer", - "format": "int64" - }, - "averageMessagesPerBatch": { - "type": "number", - "format": "double" - }, - "currentPendingMessages": { - "type": "integer", - "format": "int64" - }, - "lastBatchSentAt": { - "type": "string", - "format": "date-time" - }, - "averageBatchLatency": { - "type": "string", - "format": "date-span" - }, - "networkCallsSaved": { - "type": "integer", - "format": "int64" - }, - "isBatchingEnabled": { - "type": "boolean" - }, - "messagesByMethod": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int64" - }, - "nullable": true - }, - "batchEfficiencyPercentage": { - "type": "number", - "format": "double" - } - }, - "additionalProperties": false, - "description": "Statistics about message batching" - }, - "ChatCompletionRequest": { - "required": [ - "messages", - "model" - ], - "type": "object", - "properties": { - "model": { - "type": "string", - "nullable": true - }, - "messages": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" - }, - "nullable": true - }, - "temperature": { - "type": "number", - "format": "double", - "nullable": true - }, - "max_tokens": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "top_p": { - "type": "number", - "format": "double", - "nullable": true - }, - "top_k": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "n": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "stream": { - "type": "boolean", - "nullable": true - }, - "stop": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "user": { - "type": "string", - "nullable": true - }, - "tools": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Tool" - }, - "nullable": true - }, - "tool_choice": { - "$ref": "#/components/schemas/ToolChoice" - }, - "response_format": { - "$ref": "#/components/schemas/ResponseFormat" - }, - "seed": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "presence_penalty": { - "type": "number", - "format": "double", - "nullable": true - }, - "frequency_penalty": { - "type": "number", - "format": "double", - "nullable": true - }, - "logit_bias": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int32" - }, - "nullable": true - }, - "system_fingerprint": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": { } - }, - "ChatCompletionResponse": { - "required": [ - "choices", - "created", - "id", - "model", - "object" - ], - "type": "object", - "properties": { - "id": { - "type": "string", - "nullable": true - }, - "choices": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Choice" - }, - "nullable": true - }, - "created": { - "type": "integer", - "format": "int64" - }, - "model": { - "type": "string", - "nullable": true - }, - "system_fingerprint": { - "type": "string", - "nullable": true - }, - "object": { - "type": "string", - "nullable": true - }, - "usage": { - "$ref": "#/components/schemas/Usage" - }, - "seed": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "performance_metrics": { - "$ref": "#/components/schemas/PerformanceMetrics" - } - }, - "additionalProperties": false - }, - "Choice": { - "required": [ - "finish_reason", - "index", - "message" - ], - "type": "object", - "properties": { - "finish_reason": { - "type": "string", - "nullable": true - }, - "index": { - "type": "integer", - "format": "int32" - }, - "message": { - "$ref": "#/components/schemas/Message" - }, - "logprobs": { - "nullable": true - } - }, - "additionalProperties": false - }, - "CircuitState": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "format": "int32" - }, - "ConnectionInfo": { - "type": "object", - "properties": { - "connectionId": { - "type": "string", - "nullable": true - }, - "model": { - "type": "string", - "nullable": true - }, - "provider": { - "type": "string", - "nullable": true - }, - "connectedAt": { - "type": "string", - "format": "date-time" - }, - "state": { - "type": "string", - "nullable": true - }, - "usage": { - "$ref": "#/components/schemas/ConnectionUsageStats" - }, - "virtualKey": { - "type": "string", - "nullable": true - }, - "providerConnectionId": { - "type": "string", - "nullable": true - }, - "startTime": { - "type": "string", - "format": "date-time" - }, - "lastActivity": { - "type": "string", - "format": "date-time" - }, - "audioBytesProcessed": { - "type": "integer", - "format": "int64" - }, - "tokensUsed": { - "type": "integer", - "format": "int64" - }, - "estimatedCost": { - "type": "number", - "format": "double" - } - }, - "additionalProperties": false - }, - "ConnectionStatistics": { - "type": "object", - "properties": { - "totalActiveConnections": { - "type": "integer", - "format": "int32" - }, - "connectionsByHub": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int32" - }, - "nullable": true - }, - "connectionsByTransport": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int32" - }, - "nullable": true - }, - "totalGroups": { - "type": "integer", - "format": "int32" - }, - "staleConnections": { - "type": "integer", - "format": "int32" - }, - "averageConnectionDurationMinutes": { - "type": "number", - "format": "double" - }, - "averageIdleTimeMinutes": { - "type": "number", - "format": "double" - }, - "oldestConnectionTime": { - "type": "string", - "format": "date-time" - }, - "newestConnectionTime": { - "type": "string", - "format": "date-time" - }, - "totalMessagesSent": { - "type": "integer", - "format": "int64" - }, - "totalMessagesAcknowledged": { - "type": "integer", - "format": "int64" - }, - "acknowledgmentRate": { - "type": "number", - "format": "double" - } - }, - "additionalProperties": false, - "description": "Statistics about SignalR connections" - }, - "ConnectionStatusResponse": { - "type": "object", - "properties": { - "virtualKeyId": { - "type": "integer", - "description": "The virtual key ID.", - "format": "int32" - }, - "activeConnections": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ConnectionInfo" - }, - "description": "List of active connections.", - "nullable": true - } - }, - "additionalProperties": false, - "description": "Response model for connection status queries." - }, - "ConnectionUsageStats": { - "type": "object", - "properties": { - "audioDurationSeconds": { - "type": "number", - "format": "double" - }, - "messagesSent": { - "type": "integer", - "format": "int32" - }, - "messagesReceived": { - "type": "integer", - "format": "int32" - }, - "estimatedCost": { - "type": "number", - "format": "double" - } - }, - "additionalProperties": false - }, - "CreateSessionResponse": { - "type": "object", - "properties": { - "sessionId": { - "type": "string", - "description": "Gets or sets the created session ID.", - "nullable": true - } - }, - "additionalProperties": false, - "description": "Response for session creation." - }, - "CustomAlertRequest": { - "type": "object", - "properties": { - "severity": { - "$ref": "#/components/schemas/AlertSeverity" - }, - "title": { - "type": "string", - "nullable": true - }, - "message": { - "type": "string", - "nullable": true - }, - "component": { - "type": "string", - "nullable": true - }, - "suggestedActions": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "EmbeddingData": { - "required": [ - "embedding", - "object" - ], - "type": "object", - "properties": { - "object": { - "type": "string", - "nullable": true - }, - "embedding": { - "type": "array", - "items": { - "type": "number", - "format": "float" - }, - "nullable": true - }, - "index": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "EmbeddingRequest": { - "required": [ - "encoding_format", - "input", - "model" - ], - "type": "object", - "properties": { - "input": { - "nullable": true - }, - "model": { - "type": "string", - "nullable": true - }, - "encoding_format": { - "type": "string", - "nullable": true - }, - "dimensions": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "user": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "EmbeddingResponse": { - "required": [ - "data", - "model", - "object", - "usage" - ], - "type": "object", - "properties": { - "object": { - "type": "string", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EmbeddingData" - }, - "nullable": true - }, - "model": { - "type": "string", - "nullable": true - }, - "usage": { - "$ref": "#/components/schemas/Usage" - } - }, - "additionalProperties": false - }, - "EphemeralKeyMetadata": { - "type": "object", - "properties": { - "sourceIP": { - "type": "string", - "description": "IP address that requested the ephemeral key", - "nullable": true - }, - "userAgent": { - "type": "string", - "description": "User agent that requested the ephemeral key", - "nullable": true - }, - "purpose": { - "type": "string", - "description": "Purpose or intended use of the ephemeral key", - "nullable": true - }, - "requestId": { - "type": "string", - "description": "Request ID for correlation", - "nullable": true - } - }, - "additionalProperties": false, - "description": "Optional metadata for tracking ephemeral key usage" - }, - "EphemeralKeyResponse": { - "type": "object", - "properties": { - "ephemeralKey": { - "type": "string", - "description": "The ephemeral key token to use for authentication", - "nullable": true - }, - "expiresAt": { - "type": "string", - "description": "When the ephemeral key expires", - "format": "date-time" - }, - "expiresInSeconds": { - "type": "integer", - "description": "The TTL in seconds", - "format": "int32" - } - }, - "additionalProperties": false, - "description": "Response when creating an ephemeral key" - }, - "FunctionCall": { - "required": [ - "arguments", - "name" - ], - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true - }, - "arguments": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "FunctionDefinition": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "parameters": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/JsonNode" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "GenerateEphemeralKeyRequest": { - "type": "object", - "properties": { - "metadata": { - "$ref": "#/components/schemas/EphemeralKeyMetadata" - } - }, - "additionalProperties": false, - "description": "Request for generating an ephemeral key" - }, - "GenerateUrlRequest": { - "required": [ - "fileId" - ], - "type": "object", - "properties": { - "fileId": { - "type": "string", - "description": "The file identifier.", - "nullable": true - }, - "expirationMinutes": { - "type": "integer", - "description": "How many minutes the URL should be valid (1-10080).", - "format": "int32", - "nullable": true - } - }, - "additionalProperties": false, - "description": "Request to generate a temporary download URL." - }, - "HybridLatencyMetrics": { - "type": "object", - "properties": { - "averageSttLatencyMs": { - "type": "number", - "format": "double" - }, - "averageLlmLatencyMs": { - "type": "number", - "format": "double" - }, - "averageTtsLatencyMs": { - "type": "number", - "format": "double" - }, - "averageTotalLatencyMs": { - "type": "number", - "format": "double" - }, - "p95LatencyMs": { - "type": "number", - "format": "double" - }, - "p99LatencyMs": { - "type": "number", - "format": "double" - }, - "sampleCount": { - "type": "integer", - "format": "int32" - }, - "calculatedAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "HybridSessionConfig": { - "type": "object", - "properties": { - "sttProvider": { - "type": "string", - "nullable": true - }, - "llmModel": { - "type": "string", - "nullable": true - }, - "ttsProvider": { - "type": "string", - "nullable": true - }, - "systemPrompt": { - "type": "string", - "nullable": true - }, - "defaultVoice": { - "type": "string", - "nullable": true - }, - "maxHistoryTurns": { - "maximum": 100, - "minimum": 1, - "type": "integer", - "format": "int32" - }, - "sessionTimeout": { - "type": "string", - "format": "date-span" - }, - "enableLatencyOptimization": { - "type": "boolean" - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "ImageGenerationRequest": { - "required": [ - "model", - "prompt" - ], - "type": "object", - "properties": { - "prompt": { - "type": "string", - "nullable": true - }, - "model": { - "type": "string", - "nullable": true - }, - "n": { - "type": "integer", - "format": "int32" - }, - "quality": { - "type": "string", - "nullable": true - }, - "response_format": { - "type": "string", - "nullable": true - }, - "size": { - "type": "string", - "nullable": true - }, - "style": { - "type": "string", - "nullable": true - }, - "user": { - "type": "string", - "nullable": true - }, - "image": { - "type": "string", - "nullable": true - }, - "mask": { - "type": "string", - "nullable": true - }, - "operation": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": { } - }, - "JsonNode": { - "type": "object", - "properties": { - "options": { - "$ref": "#/components/schemas/JsonNodeOptions" - }, - "parent": { - "$ref": "#/components/schemas/JsonNode" - }, - "root": { - "$ref": "#/components/schemas/JsonNode" - } - }, - "additionalProperties": false - }, - "JsonNodeOptions": { - "type": "object", - "properties": { - "propertyNameCaseInsensitive": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "Message": { - "required": [ - "role" - ], - "type": "object", - "properties": { - "role": { - "type": "string", - "nullable": true - }, - "content": { - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "tool_calls": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ToolCall" - }, - "nullable": true - }, - "tool_call_id": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "OpenAIError": { - "required": [ - "message", - "type" - ], - "type": "object", - "properties": { - "message": { - "type": "string", - "nullable": true - }, - "type": { - "type": "string", - "nullable": true - }, - "param": { - "type": "string", - "nullable": true - }, - "code": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "OpenAIErrorResponse": { - "required": [ - "error" - ], - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/OpenAIError" - } - }, - "additionalProperties": false - }, - "PerformanceMetrics": { - "type": "object", - "properties": { - "total_latency_ms": { - "type": "integer", - "format": "int64" - }, - "time_to_first_token_ms": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "tokens_per_second": { - "type": "number", - "format": "double", - "nullable": true - }, - "prompt_tokens_per_second": { - "type": "number", - "format": "double", - "nullable": true - }, - "completion_tokens_per_second": { - "type": "number", - "format": "double", - "nullable": true - }, - "provider": { - "type": "string", - "nullable": true - }, - "model": { - "type": "string", - "nullable": true - }, - "streaming": { - "type": "boolean" - }, - "retry_attempts": { - "type": "integer", - "format": "int32" - }, - "avg_inter_token_latency_ms": { - "type": "number", - "format": "double", - "nullable": true - } - }, - "additionalProperties": false - }, - "ProblemDetails": { - "type": "object", - "properties": { - "type": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string", - "nullable": true - }, - "status": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "detail": { - "type": "string", - "nullable": true - }, - "instance": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": { } - }, - "ProviderType": { - "enum": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11 - ], - "type": "integer", - "format": "int32" - }, - "QueueStatistics": { - "type": "object", - "properties": { - "pendingMessages": { - "type": "integer", - "format": "int32" - }, - "deadLetterMessages": { - "type": "integer", - "format": "int32" - }, - "processedMessages": { - "type": "integer", - "format": "int32" - }, - "failedMessages": { - "type": "integer", - "format": "int32" - }, - "lastProcessedAt": { - "type": "string", - "format": "date-time" - }, - "circuitBreakerState": { - "$ref": "#/components/schemas/CircuitState" - }, - "consecutiveFailures": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false, - "description": "Statistics about the message queue" - }, - "ResponseFormat": { - "type": "object", - "properties": { - "type": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "SearchUsageMetadata": { - "type": "object", - "properties": { - "query_count": { - "type": "integer", - "format": "int32" - }, - "document_count": { - "type": "integer", - "format": "int32" - }, - "chunked_document_count": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "ServiceStatus": { - "type": "object", - "properties": { - "available": { - "type": "boolean", - "description": "Gets or sets whether the service is available." - }, - "latencyMetrics": { - "$ref": "#/components/schemas/HybridLatencyMetrics" - } - }, - "additionalProperties": false, - "description": "Service status response." - }, - "SpendUpdateDto": { - "required": [ - "amount", - "model", - "providerType", - "virtualKeyId" - ], - "type": "object", - "properties": { - "virtualKeyId": { - "type": "integer", - "format": "int32" - }, - "amount": { - "maximum": 1000000, - "minimum": 0.0001, - "type": "number", - "format": "double" - }, - "model": { - "minLength": 1, - "type": "string" - }, - "providerType": { - "$ref": "#/components/schemas/ProviderType" - }, - "metadata": { - "type": "object", - "additionalProperties": { }, - "nullable": true - } - }, - "additionalProperties": false - }, - "TextToSpeechRequestDto": { - "required": [ - "input", - "model", - "voice" - ], - "type": "object", - "properties": { - "model": { - "minLength": 1, - "type": "string" - }, - "input": { - "minLength": 1, - "type": "string" - }, - "voice": { - "minLength": 1, - "type": "string" - }, - "response_format": { - "type": "string", - "nullable": true - }, - "speed": { - "maximum": 4, - "minimum": 0.25, - "type": "number", - "format": "double", - "nullable": true - } - }, - "additionalProperties": false - }, - "Tool": { - "required": [ - "function" - ], - "type": "object", - "properties": { - "type": { - "type": "string", - "nullable": true - }, - "function": { - "$ref": "#/components/schemas/FunctionDefinition" - } - }, - "additionalProperties": false - }, - "ToolCall": { - "required": [ - "function", - "id" - ], - "type": "object", - "properties": { - "id": { - "type": "string", - "nullable": true - }, - "type": { - "type": "string", - "nullable": true - }, - "function": { - "$ref": "#/components/schemas/FunctionCall" - } - }, - "additionalProperties": false - }, - "ToolChoice": { - "type": "object", - "additionalProperties": false - }, - "TranscriptionAlternative": { - "type": "object", - "properties": { - "text": { - "type": "string", - "nullable": true - }, - "confidence": { - "type": "number", - "format": "double" - }, - "segments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TranscriptionSegment" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "TranscriptionSegment": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "start": { - "type": "number", - "format": "double" - }, - "end": { - "type": "number", - "format": "double" - }, - "text": { - "type": "string", - "nullable": true - }, - "confidence": { - "type": "number", - "format": "double", - "nullable": true - }, - "speaker": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "TranscriptionWord": { - "type": "object", - "properties": { - "word": { - "type": "string", - "nullable": true - }, - "start": { - "type": "number", - "format": "double" - }, - "end": { - "type": "number", - "format": "double" - }, - "confidence": { - "type": "number", - "format": "double", - "nullable": true - }, - "speaker": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "Usage": { - "type": "object", - "properties": { - "prompt_tokens": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "completion_tokens": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "total_tokens": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "image_count": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "video_duration_seconds": { - "type": "number", - "format": "double", - "nullable": true - }, - "video_resolution": { - "type": "string", - "nullable": true - }, - "is_batch": { - "type": "boolean", - "nullable": true - }, - "image_quality": { - "type": "string", - "nullable": true - }, - "image_resolution": { - "type": "string", - "nullable": true - }, - "cached_input_tokens": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "cached_write_tokens": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "search_units": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "search_metadata": { - "$ref": "#/components/schemas/SearchUsageMetadata" - }, - "inference_steps": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "audio_duration_seconds": { - "type": "number", - "format": "double", - "nullable": true - }, - "metadata": { - "type": "object", - "additionalProperties": { }, - "nullable": true - } - }, - "additionalProperties": { } - }, - "VideoData": { - "type": "object", - "properties": { - "url": { - "type": "string", - "nullable": true - }, - "b64_json": { - "type": "string", - "nullable": true - }, - "metadata": { - "$ref": "#/components/schemas/VideoMetadata" - }, - "revised_prompt": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "VideoGenerationRequest": { - "required": [ - "model", - "prompt" - ], - "type": "object", - "properties": { - "prompt": { - "type": "string", - "nullable": true - }, - "model": { - "type": "string", - "nullable": true - }, - "duration": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "size": { - "type": "string", - "nullable": true - }, - "fps": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "style": { - "type": "string", - "nullable": true - }, - "response_format": { - "type": "string", - "nullable": true - }, - "user": { - "type": "string", - "nullable": true - }, - "seed": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "n": { - "type": "integer", - "format": "int32" - }, - "webhook_url": { - "type": "string", - "nullable": true - }, - "webhook_headers": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "nullable": true - } - }, - "additionalProperties": { } - }, - "VideoGenerationResponse": { - "type": "object", - "properties": { - "created": { - "type": "integer", - "format": "int64" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VideoData" - }, - "nullable": true - }, - "model": { - "type": "string", - "nullable": true - }, - "usage": { - "$ref": "#/components/schemas/VideoGenerationUsage" - } - }, - "additionalProperties": false - }, - "VideoGenerationTaskResponse": { - "type": "object", - "properties": { - "taskId": { - "type": "string", - "description": "Unique identifier for the video generation task.", - "nullable": true - }, - "status": { - "type": "string", - "description": "Current status of the task (pending, processing, completed, failed).", - "nullable": true - }, - "createdAt": { - "type": "string", - "description": "When the task was created.", - "format": "date-time" - }, - "estimatedCompletionTime": { - "type": "string", - "description": "Estimated time when the video will be ready.", - "format": "date-time", - "nullable": true - }, - "checkStatusUrl": { - "type": "string", - "description": "URL to check the status of this task.", - "nullable": true - } - }, - "additionalProperties": false, - "description": "Response for async video generation task creation." - }, - "VideoGenerationTaskStatus": { - "type": "object", - "properties": { - "taskId": { - "type": "string", - "description": "Unique identifier for the task.", - "nullable": true - }, - "status": { - "type": "string", - "description": "Current status (pending, running, completed, failed, cancelled).", - "nullable": true - }, - "progress": { - "type": "integer", - "description": "Progress percentage (0-100).", - "format": "int32", - "nullable": true - }, - "createdAt": { - "type": "string", - "description": "When the task was created.", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "When the task was last updated.", - "format": "date-time" - }, - "completedAt": { - "type": "string", - "description": "When the task completed (if applicable).", - "format": "date-time", - "nullable": true - }, - "error": { - "type": "string", - "description": "Error message if the task failed.", - "nullable": true - }, - "result": { - "type": "string", - "description": "Result data (internal use).", - "nullable": true - }, - "videoResponse": { - "$ref": "#/components/schemas/VideoGenerationResponse" - } - }, - "additionalProperties": false, - "description": "Status information for a video generation task." - }, - "VideoGenerationUsage": { - "type": "object", - "properties": { - "videos_generated": { - "type": "integer", - "format": "int32" - }, - "total_duration_seconds": { - "type": "number", - "format": "double" - }, - "estimated_cost": { - "type": "number", - "format": "double", - "nullable": true - } - }, - "additionalProperties": false - }, - "VideoMetadata": { - "type": "object", - "properties": { - "width": { - "type": "integer", - "format": "int32" - }, - "height": { - "type": "integer", - "format": "int32" - }, - "duration": { - "type": "number", - "format": "double" - }, - "fps": { - "type": "number", - "format": "double" - }, - "codec": { - "type": "string", - "nullable": true - }, - "audio_codec": { - "type": "string", - "nullable": true - }, - "file_size_bytes": { - "type": "integer", - "format": "int64" - }, - "bitrate": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "mime_type": { - "type": "string", - "nullable": true - }, - "format": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "VirtualKeyUpdateDto": { - "required": [ - "virtualKeyId" - ], - "type": "object", - "properties": { - "virtualKeyId": { - "type": "integer", - "format": "int32" - }, - "maxBudget": { - "maximum": 1000000, - "minimum": 0, - "type": "number", - "format": "double", - "nullable": true - }, - "allowedModels": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "rateLimits": { - "type": "object", - "additionalProperties": { }, - "nullable": true - }, - "isEnabled": { - "type": "boolean", - "nullable": true - }, - "expiresAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "notes": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "WebhookSendDto": { - "required": [ - "eventType", - "payload", - "url" - ], - "type": "object", - "properties": { - "url": { - "minLength": 1, - "type": "string", - "format": "uri" - }, - "eventType": { - "minLength": 1, - "type": "string" - }, - "payload": { }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "nullable": true - }, - "secret": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - } - }, - "securitySchemes": { - "ApiKey": { - "type": "apiKey", - "description": "Virtual Key authentication using Authorization header", - "name": "Authorization", - "in": "header" - } - } - }, - "security": [ - { - "ApiKey": [ ] - } - ] -} \ No newline at end of file diff --git a/ConduitLLM.IntegrationTests/ConduitLLM.IntegrationTests.csproj b/ConduitLLM.IntegrationTests/ConduitLLM.IntegrationTests.csproj deleted file mode 100644 index 0da2ff1f6..000000000 --- a/ConduitLLM.IntegrationTests/ConduitLLM.IntegrationTests.csproj +++ /dev/null @@ -1,46 +0,0 @@ - - - - net9.0 - enable - enable - false - true - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - - - - \ No newline at end of file diff --git a/ConduitLLM.IntegrationTests/README.md b/ConduitLLM.IntegrationTests/README.md deleted file mode 100644 index 17fe1605c..000000000 --- a/ConduitLLM.IntegrationTests/README.md +++ /dev/null @@ -1,210 +0,0 @@ -# ConduitLLM Integration Tests - -End-to-end integration tests for Conduit that verify the complete flow from provider setup through virtual key billing. - -## Overview - -These integration tests verify the complete functionality of Conduit by: -1. Creating a provider with API keys -2. Setting up model mappings and costs -3. Creating virtual key groups with credit -4. Sending chat requests through virtual keys -5. Verifying token tracking and billing accuracy - -## Prerequisites - -1. **Docker Environment Running**: Start the development environment: - ```bash - ./scripts/start-dev.sh - ``` - -2. **Services Health**: The tests will automatically wait for all services to be healthy: - - Core API (http://localhost:5000) - - Admin API (http://localhost:5002) - - PostgreSQL - - Redis - - RabbitMQ (if configured) - -## Configuration - -### Step 1: Copy Configuration Templates - -```bash -cd ConduitLLM.IntegrationTests - -# Copy main configuration -cp Config/test-config.template.yaml Config/test-config.yaml - -# Copy provider configuration -cp Config/providers/groq.template.yaml Config/providers/groq.yaml -``` - -### Step 2: Configure test-config.yaml - -Edit `Config/test-config.yaml` and set: -- `adminApiKey`: Get from `docker-compose.dev.yml` (CONDUIT_API_TO_API_BACKEND_AUTH_KEY) -- Leave other settings as default unless you have custom configurations - -### Step 3: Configure Provider API Keys - -Edit `Config/providers/groq.yaml` and add your Groq API key: -```yaml -provider: - apiKey: "gsk_YOUR_ACTUAL_GROQ_API_KEY_HERE" -``` - -## Running Tests - -### Run All Tests -```bash -cd ConduitLLM.IntegrationTests -dotnet test -``` - -### Run with Detailed Output -```bash -dotnet test --logger "console;verbosity=detailed" -``` - -### Run Specific Test Step -```bash -dotnet test --filter "FullyQualifiedName~Step01" -``` - -## Test Flow - -The test executes the following steps in order: - -1. **Step01_CreateProvider**: Creates a TEST_ prefixed provider -2. **Step02_CreateProviderKey**: Adds API key as default and primary -3. **Step03_CreateModelMapping**: Maps TEST_gemma2-9b to gemma2-9b-it -4. **Step04_CreateModelCost**: Sets pricing at $0.20 per million tokens -5. **Step05_CreateVirtualKeyGroup**: Creates group with $100 credit -6. **Step06_CreateVirtualKey**: Generates virtual key for API access -7. **Step07_SendChatRequest**: Sends "What is the history of France?" -8. **Step08_VerifyTokenTracking**: Validates token counts match provider response -9. **Step09_VerifyVirtualKeyDebit**: Confirms accurate billing (micro-cents precision) -10. **Step10_GenerateReport**: Creates markdown report in Reports/ directory - -## Test Reports - -After each test run, a detailed markdown report is generated: -- Location: `Reports/test_run_{timestamp}.md` -- Contents: - - Provider setup details - - Token usage and costs - - Response validation results - - Any errors encountered - -## Debugging Failed Tests - -### Test Context -The test saves its state to `test-context.json` after each step. This file contains: -- All created entity IDs -- Last chat response details -- Cost calculations -- Error messages - -### Manual Database Inspection -All test entities are prefixed with `TEST_` for easy identification: -```sql --- View test providers -SELECT * FROM "Providers" WHERE "Name" LIKE 'TEST_%'; - --- View test virtual keys -SELECT * FROM "VirtualKeys" WHERE "Name" LIKE 'TEST_%'; - --- View test transactions -SELECT * FROM "VirtualKeySpendHistory" -WHERE "VirtualKey" IN ( - SELECT "VirtualKey" FROM "VirtualKeys" WHERE "Name" LIKE 'TEST_%' -); -``` - -## Expanding Tests - -### Adding New Providers - -1. Create provider configuration: - ```bash - cp Config/providers/groq.template.yaml Config/providers/openai.template.yaml - # Edit to match OpenAI specifics - ``` - -2. Add to active providers in `test-config.yaml`: - ```yaml - activeProviders: - - groq - - openai - ``` - -3. Future: The test framework is designed to support: - - All provider types (OpenAI, Anthropic, Cerebras, etc.) - - Multimodal inputs (images with chat) - - Image generation testing - - Video generation testing - -### Test Data Persistence - -Tests intentionally DO NOT clean up data to allow: -- Manual verification of results -- Debugging of failures -- Audit trail of test runs - -To clean test data manually: -```sql -DELETE FROM "VirtualKeySpendHistory" WHERE "VirtualKey" IN ( - SELECT "VirtualKey" FROM "VirtualKeys" WHERE "Name" LIKE 'TEST_%' -); -DELETE FROM "VirtualKeys" WHERE "Name" LIKE 'TEST_%'; -DELETE FROM "VirtualKeyGroups" WHERE "Name" LIKE 'TEST_%'; -DELETE FROM "ModelCosts" WHERE "ModelPattern" LIKE 'TEST_%'; -DELETE FROM "ModelProviderMappings" WHERE "ModelAlias" LIKE 'TEST_%'; -DELETE FROM "ProviderKeyCredentials" WHERE "ProviderId" IN ( - SELECT "Id" FROM "Providers" WHERE "Name" LIKE 'TEST_%' -); -DELETE FROM "Providers" WHERE "Name" LIKE 'TEST_%'; -``` - -## Common Issues - -### Build Errors -- Ensure you're using .NET 9.0 SDK -- Run `dotnet restore` if package errors occur - -### Configuration Not Found -- Ensure you copied the template files -- Check file names match exactly (case-sensitive) - -### API Authentication Fails -- Verify `adminApiKey` matches `CONDUIT_API_TO_API_BACKEND_AUTH_KEY` in docker-compose.dev.yml -- Ensure services are running with `docker ps` - -### Provider API Errors -- Verify your API key is valid and has credits -- Check provider service status -- Review rate limits for your API key tier - -### Test Timeouts -- Default chat timeout is 60 seconds -- Adjust in `test-config.yaml` if needed for slower providers - -## Cost Tracking - -Each test run uses minimal API credits (typically < $0.01): -- Groq gemma2-9b: ~$0.000051 per test -- Costs are tracked with micro-cent precision (6 decimal places) -- Virtual key starts with $100 credit (configurable) - -## TODO: Future Enhancements - -- [ ] Parallel provider testing -- [ ] Multimodal input testing (images) -- [ ] Image generation verification -- [ ] Video generation verification -- [ ] Streaming response validation -- [ ] Rate limiting tests -- [ ] Error handling scenarios -- [ ] Performance benchmarking -- [ ] Load testing capabilities -- [ ] CI/CD integration \ No newline at end of file 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 deleted file mode 100644 index 63ec9c211..000000000 --- a/ConduitLLM.Providers/Common/Models/ModelCapabilities.cs +++ /dev/null @@ -1,99 +0,0 @@ -using ConduitLLM.Core.Interfaces; - -namespace ConduitLLM.Providers.Common.Models -{ - /// - /// Represents the capabilities of a specific model. - /// - public class ModelCapabilities - { - /// - /// Gets or sets a value indicating whether the model supports chat completions. - /// - public bool Chat { get; set; } - - /// - /// Gets or sets a value indicating whether the model supports text generation. - /// - public bool TextGeneration { get; set; } - - /// - /// Gets or sets a value indicating whether the model supports embeddings. - /// - public bool Embeddings { get; set; } - - /// - /// Gets or sets a value indicating whether the model supports image generation. - /// - public bool ImageGeneration { get; set; } - - /// - /// Gets or sets a value indicating whether the model supports vision/multimodal inputs. - /// - public bool Vision { get; set; } - - /// - /// Gets or sets a value indicating whether the model supports function calling. - /// - public bool FunctionCalling { get; set; } - - /// - /// Gets or sets a value indicating whether the model supports tool usage. - /// - public bool ToolUsage { get; set; } - - /// - /// Gets or sets a value indicating whether the model supports JSON mode. - /// - 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. - /// - public bool VideoGeneration { get; set; } - - /// - /// Converts the capabilities to a dictionary for serialization. - /// - /// A dictionary representation of the capabilities. - public Dictionary ToDictionary() - { - return new Dictionary - { - [nameof(Chat)] = Chat, - [nameof(TextGeneration)] = TextGeneration, - [nameof(Embeddings)] = Embeddings, - [nameof(ImageGeneration)] = ImageGeneration, - [nameof(Vision)] = Vision, - [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/ConduitLLM.Providers.csproj b/ConduitLLM.Providers/ConduitLLM.Providers.csproj deleted file mode 100644 index f349f0cb5..000000000 --- a/ConduitLLM.Providers/ConduitLLM.Providers.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - net9.0 - enable - enable - - - 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/Groq/GroqClient.Chat.cs b/ConduitLLM.Providers/Providers/Groq/GroqClient.Chat.cs deleted file mode 100644 index dc181921f..000000000 --- a/ConduitLLM.Providers/Providers/Groq/GroqClient.Chat.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Runtime.CompilerServices; - -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.Groq -{ - /// - /// GroqClient partial class containing chat completion methods. - /// - public partial class GroqClient - { - /// - /// Creates a chat completion with enhanced error handling specific to Groq. - /// - /// The chat completion request. - /// Optional API key to override the one in credentials. - /// A token to monitor for cancellation requests. - /// A chat completion response from Groq. - /// Thrown when there is a communication error with Groq. - public override async Task CreateChatCompletionAsync( - ChatCompletionRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - try - { - return await base.CreateChatCompletionAsync(request, apiKey, cancellationToken); - } - catch (LLMCommunicationException ex) - { - // Enhance error message handling for Groq and re-throw - var enhancedErrorMessage = ExtractEnhancedErrorMessage(ex); - throw new LLMCommunicationException(enhancedErrorMessage, ex); - } - catch (Exception ex) when (ex is not OperationCanceledException) - { - // Handle other exceptions not caught by the base class - var errorMessage = ex.Message; - if (ex is HttpRequestException httpEx && httpEx.Data["Body"] is string body) - { - errorMessage = body; - } - - Logger.LogError(ex, "Groq API error: {Message}", errorMessage); - throw new LLMCommunicationException($"Groq API error: {errorMessage}", ex); - } - } - - /// - /// Streams a chat completion with enhanced error handling specific to Groq. - /// - /// The chat completion request. - /// Optional API key to override the one in credentials. - /// A token to monitor for cancellation requests. - /// An async enumerable of chat completion chunks. - /// Thrown when there is a communication error with Groq. - public override async IAsyncEnumerable StreamChatCompletionAsync( - ChatCompletionRequest request, - string? apiKey = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - // Create a wrapped stream to avoid yielding in try/catch - IAsyncEnumerable baseStream; - - try - { - // Get the base implementation's stream - baseStream = base.StreamChatCompletionAsync(request, apiKey, cancellationToken); - } - catch (Exception ex) when (ex is not OperationCanceledException) - { - // Enhance error message handling for Groq - var enhancedErrorMessage = ExtractEnhancedErrorMessage(ex); - Logger.LogError(ex, "Error initializing streaming chat completion from Groq: {Message}", enhancedErrorMessage); - throw new LLMCommunicationException(enhancedErrorMessage, ex); - } - - // Process the stream outside of try/catch - await foreach (var chunk in baseStream.WithCancellation(cancellationToken)) - { - yield return chunk; - } - } - } -} 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.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 deleted file mode 100644 index d50abc12d..000000000 --- a/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.Utilities.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.OpenAI -{ - /// - /// OpenAIClient partial class containing utility methods. - /// - 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; - } - } -} \ No newline at end of file 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/Replicate/ReplicateClient.Media.cs b/ConduitLLM.Providers/Providers/Replicate/ReplicateClient.Media.cs deleted file mode 100644 index a477d2d1b..000000000 --- a/ConduitLLM.Providers/Providers/Replicate/ReplicateClient.Media.cs +++ /dev/null @@ -1,209 +0,0 @@ -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.Replicate -{ - public partial class ReplicateClient - { - /// - public override async Task CreateImageAsync( - ImageGenerationRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - ValidateRequest(request, "CreateImageAsync"); - - Logger.LogInformation("Creating image with Replicate for model '{ModelId}'", ProviderModelId); - - try - { - // Map the request to Replicate format and start prediction - var predictionRequest = MapToImageGenerationRequest(request); - var predictionResponse = await StartPredictionAsync(predictionRequest, apiKey, cancellationToken); - - // Poll until prediction completes or fails - var finalPrediction = await PollPredictionUntilCompletedAsync(predictionResponse.Id, apiKey, cancellationToken); - - // Process the final result - return MapToImageGenerationResponse(finalPrediction, request.Model); - } - catch (LLMCommunicationException) - { - // Re-throw LLMCommunicationException directly - throw; - } - catch (Exception ex) - { - Logger.LogError(ex, "An unexpected error occurred while processing Replicate image generation"); - throw new LLMCommunicationException($"An unexpected error occurred: {ex.Message}", ex); - } - } - - /// - /// Creates a video using Replicate's prediction API. - /// - /// The video generation request containing the prompt and generation parameters. - /// Optional API key override to use instead of the client's configured key. - /// A token to cancel the operation. - /// The video generation response containing URLs to the generated video(s). - public async Task CreateVideoAsync( - VideoGenerationRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - ValidateRequest(request, "CreateVideoAsync"); - - Logger.LogInformation("Creating video with Replicate for model '{ModelId}'", ProviderModelId); - - try - { - // Map the request to Replicate format and start prediction - var predictionRequest = MapToVideoGenerationRequest(request); - var predictionResponse = await StartPredictionAsync(predictionRequest, apiKey, cancellationToken); - - // Poll until prediction completes or fails - var finalPrediction = await PollPredictionUntilCompletedAsync(predictionResponse.Id, apiKey, cancellationToken); - - // Process the final result - return MapToVideoGenerationResponse(finalPrediction, request.Model); - } - catch (LLMCommunicationException) - { - // Re-throw LLMCommunicationException directly - throw; - } - catch (Exception ex) - { - Logger.LogError(ex, "An unexpected error occurred while processing Replicate video generation"); - throw new LLMCommunicationException($"An unexpected error occurred: {ex.Message}", ex); - } - } - - private ReplicatePredictionRequest MapToImageGenerationRequest(ImageGenerationRequest request) - { - // Prepare the input based on the model - var input = new Dictionary - { - ["prompt"] = request.Prompt - }; - - // Add optional parameters if provided - if (request.Size != null) - { - var dimensions = request.Size.Split('x'); - if (dimensions.Length == 2 && int.TryParse(dimensions[0], out int width) && int.TryParse(dimensions[1], out int height)) - { - input["width"] = width; - input["height"] = height; - } - } - - if (request.Quality != null) - { - input["quality"] = request.Quality; - } - - if (request.Style != null) - { - input["style"] = request.Style; - } - - if (request.N > 1) - { - input["num_outputs"] = request.N; - } - - return new ReplicatePredictionRequest - { - Version = ProviderModelId, - Input = input - }; - } - - private ReplicatePredictionRequest MapToVideoGenerationRequest(VideoGenerationRequest request) - { - // Prepare the input based on the model - var input = new Dictionary - { - ["prompt"] = request.Prompt - }; - - // Add optional parameters if provided - if (request.Duration.HasValue) - { - // Most video models use "duration" or "num_seconds" - input["duration"] = request.Duration.Value; - input["num_seconds"] = request.Duration.Value; - } - - if (request.Size != null) - { - // Parse size like "1280x720" into width and height - var dimensions = request.Size.Split('x'); - if (dimensions.Length == 2 && int.TryParse(dimensions[0], out int width) && int.TryParse(dimensions[1], out int height)) - { - input["width"] = width; - input["height"] = height; - } - else - { - // Some models use resolution directly - input["resolution"] = request.Size; - } - } - - if (request.Fps.HasValue) - { - input["fps"] = request.Fps.Value; - } - - if (request.Seed.HasValue) - { - input["seed"] = request.Seed.Value; - } - - if (request.N > 1) - { - input["num_outputs"] = request.N; - } - - return new ReplicatePredictionRequest - { - Version = ProviderModelId, - Input = input - }; - } - - private ImageGenerationResponse MapToImageGenerationResponse(ReplicatePredictionResponse prediction, string originalModelAlias) - { - // Extract image URLs from the prediction output - var imageUrls = ExtractImageUrlsFromPredictionOutput(prediction.Output); - - return new ImageGenerationResponse - { - Created = ((DateTimeOffset)prediction.CreatedAt).ToUnixTimeSeconds(), - Data = imageUrls.Select(url => new Core.Models.ImageData - { - Url = url - }).ToList() - }; - } - - private VideoGenerationResponse MapToVideoGenerationResponse(ReplicatePredictionResponse prediction, string originalModelAlias) - { - // Extract video URLs from the prediction output - var videoUrls = ExtractVideoUrlsFromPredictionOutput(prediction.Output); - - return new VideoGenerationResponse - { - Created = ((DateTimeOffset)prediction.CreatedAt).ToUnixTimeSeconds(), - Data = videoUrls.Select(url => new VideoData - { - Url = url - }).ToList() - }; - } - } -} \ No newline at end of file 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/README.md b/ConduitLLM.Providers/README.md deleted file mode 100644 index 687b91906..000000000 --- a/ConduitLLM.Providers/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# ConduitLLM.Providers - -## Overview - -**ConduitLLM.Providers** is a core library within the [ConduitLLM](../) solution, responsible for providing modular, extensible integration with various Large Language Model (LLM) backends and services. It abstracts the details of interacting with different LLM providers and exposes a uniform interface for the rest of the Conduit ecosystem. - -This project is part of the larger `Conduit.sln` solution, which is composed of several sub-projects: - -- **ConduitLLM.Http**: The HTTP API layer that exposes LLM functionality via REST endpoints. -- **ConduitLLM.WebUI**: The web-based user interface for interacting with LLMs. -- **ConduitLLM.Configuration**: Centralized configuration and settings management. -- **ConduitLLM.Providers**: (This project) LLM provider abstraction and implementations. -- **Other sub-projects**: May include Caching, Utilities, or Domain-specific libraries. - -## How It Fits Into Conduit - -The Providers library is consumed by both the API and UI layers. It allows the Conduit solution to support multiple LLM backends (e.g., OpenAI, Azure OpenAI, HuggingFace, local models) without requiring changes in the API or UI code. New providers can be added by implementing the appropriate interfaces. - -``` -[WebUI] <---> [Http API] <---> [Providers] <---> [LLM Backends] - ^ - | - [Configuration] -``` - -## Features - -- Unified interface for LLM operations (completion, chat, embeddings, etc.) -- Plug-and-play support for multiple LLM vendors -- Extensible: add custom providers by implementing interfaces -- Centralized provider configuration (via ConduitLLM.Configuration) -- Dependency injection friendly - -## Usage - -### Adding a Provider - -To add a new provider, implement the relevant interfaces (e.g., `ILLMProvider`, `IChatProvider`) and register your implementation in the DI container (typically in the API or startup project). - -### Example: Registering Providers - -```csharp -services.AddLLMProviders(Configuration); -``` - -This will scan and register all available providers based on your configuration. - -### Consuming a Provider - -Inject the desired interface into your service or controller: - -```csharp -public class MyService -{ - private readonly ILLMProvider _llmProvider; - public MyService(ILLMProvider llmProvider) { _llmProvider = llmProvider; } - - public async Task GetCompletion(string prompt) - { - return await _llmProvider.CompleteAsync(prompt); - } -} -``` - -## Configuration - -Provider selection and credentials are configured via environment variables or configuration files, typically managed by the `ConduitLLM.Configuration` project. - -Example environment variables (names may vary by provider): - -- `OpenAI__ApiKey` -- `AzureOpenAI__Endpoint` -- `HuggingFace__Token` -- `DefaultProvider=OpenAI` - -You can set these in your environment, `appsettings.json`, or through Docker Compose. - -## Extending - -To add a new provider: - -1. Implement the provider interface(s). -2. Register the provider in the DI container. -3. Add configuration schema in `ConduitLLM.Configuration` if needed. - -## Dependencies - -- .NET 7.0+ (or as specified by the solution) -- Dependency Injection (Microsoft.Extensions.DependencyInjection) -- Configuration (Microsoft.Extensions.Configuration) - -## Development - -- All code should be unit tested. -- Follow the project’s code style and contribution guidelines. -- PRs for new providers should include documentation and tests. - -## License - -This project is part of the ConduitLLM solution and inherits its license. 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.Security/ConduitLLM.Security.csproj b/ConduitLLM.Security/ConduitLLM.Security.csproj deleted file mode 100644 index 9dcfc2743..000000000 --- a/ConduitLLM.Security/ConduitLLM.Security.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - net9.0 - enable - enable - true - $(NoWarn);1591 - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ConduitLLM.Security/README.md b/ConduitLLM.Security/README.md deleted file mode 100644 index 31806687f..000000000 --- a/ConduitLLM.Security/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# ConduitLLM.Security - -This project contains shared security services, interfaces, and middleware used by both the Core API (ConduitLLM.Http) and Admin API (ConduitLLM.Admin). - -## Overview - -The ConduitLLM.Security project provides: -- Security event monitoring and logging -- Threat detection and analysis -- Security metrics collection -- Shared authentication handlers -- Security middleware components -- Rate limiting and IP filtering logic - -## Project Structure - -``` -ConduitLLM.Security/ -├── Services/ # Security service implementations -├── Interfaces/ # Security service interfaces -├── Models/ # Internal security models -├── Middleware/ # Security middleware components -└── Extensions/ # Extension methods for security configuration -``` - -## Key Components - -### Services -- `SecurityEventMonitoringService` - Monitors and logs security events -- `ThreatDetectionService` - Analyzes patterns for threat detection -- `SecurityMetricsService` - Collects and aggregates security metrics - -### Interfaces -- `ISecurityEventMonitoringService` - Security event monitoring contract -- `IThreatDetectionService` - Threat detection contract -- `ISecurityMetricsService` - Security metrics contract - -### Middleware -- Shared security middleware for authentication and authorization -- Rate limiting middleware -- IP filtering middleware - -## Usage - -Both Core API and Admin API reference this project to access shared security functionality. The services are registered via dependency injection and can be used throughout the application. - -## Dependencies - -- ConduitLLM.Core - Core interfaces and models -- ConduitLLM.Configuration - Security DTOs and configuration \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Controllers/GlobalSettingsControllerTests.Constructor.cs b/ConduitLLM.Tests/Admin/Controllers/GlobalSettingsControllerTests.Constructor.cs deleted file mode 100644 index c2e0edbb4..000000000 --- a/ConduitLLM.Tests/Admin/Controllers/GlobalSettingsControllerTests.Constructor.cs +++ /dev/null @@ -1,27 +0,0 @@ -using ConduitLLM.Admin.Controllers; - -namespace ConduitLLM.Tests.Admin.Controllers -{ - public partial class GlobalSettingsControllerTests - { - #region Constructor Tests - - [Fact] - public void Constructor_WithNullService_ShouldThrowArgumentNullException() - { - // Act & Assert - Assert.Throws(() => - new GlobalSettingsController(null!, _mockLogger.Object)); - } - - [Fact] - public void Constructor_WithNullLogger_ShouldThrowArgumentNullException() - { - // Act & Assert - Assert.Throws(() => - new GlobalSettingsController(_mockService.Object, null!)); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Controllers/ModelControllerTests.ProviderOperations.cs b/ConduitLLM.Tests/Admin/Controllers/ModelControllerTests.ProviderOperations.cs deleted file mode 100644 index 59d742fec..000000000 --- a/ConduitLLM.Tests/Admin/Controllers/ModelControllerTests.ProviderOperations.cs +++ /dev/null @@ -1,380 +0,0 @@ -using ConduitLLM.Admin.Controllers; -using ConduitLLM.Admin.Models.Models; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.Repositories; - -using FluentAssertions; - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -using Moq; - -namespace ConduitLLM.Tests.Admin.Controllers -{ - /// - /// Unit tests for ModelController provider-related operations - /// - [Trait("Category", "Unit")] - [Trait("Component", "AdminController")] - public class ModelControllerProviderOperationsTests - { - private readonly Mock _mockRepository; - private readonly Mock> _mockLogger; - private readonly ModelController _controller; - - public ModelControllerProviderOperationsTests() - { - _mockRepository = new Mock(); - _mockLogger = new Mock>(); - _controller = new ModelController(_mockRepository.Object, _mockLogger.Object); - } - - #region GetModelsByProvider Tests - - [Fact] - public async Task GetModelsByProvider_WithValidProvider_ShouldReturnOkWithModels() - { - // Arrange - var provider = "groq"; - var author = new ModelAuthor { Id = 1, Name = "Test Author" }; - var series = new ModelSeries { Id = 1, Name = "Test Series", Author = author }; - var capabilities = new ModelCapabilities - { - Id = 1, - SupportsChat = true, - MaxTokens = 4096 - }; - - var models = new List - { - new Model - { - Id = 1, - Name = "llama-3.1-8b", - ModelSeriesId = 1, - Series = series, - ModelCapabilitiesId = 1, - Capabilities = capabilities, - IsActive = true, - Identifiers = new List - { - new ModelIdentifier - { - Id = 1, - ModelId = 1, - Identifier = "llama-3.1-8b-instant", - Provider = "groq", - IsPrimary = true - } - } - }, - new Model - { - Id = 2, - Name = "mixtral-8x7b", - ModelSeriesId = 1, - Series = series, - ModelCapabilitiesId = 1, - Capabilities = capabilities, - IsActive = true, - Identifiers = new List - { - new ModelIdentifier - { - Id = 2, - ModelId = 2, - Identifier = "mixtral-8x7b-32768", - Provider = "groq", - IsPrimary = true - } - } - } - }; - - _mockRepository.Setup(r => r.GetByProviderAsync(provider)) - .ReturnsAsync(models); - - // Act - var result = await _controller.GetModelsByProvider(provider); - - // Assert - result.Should().BeOfType(); - var okResult = result as OkObjectResult; - okResult.Should().NotBeNull(); - - var dtos = okResult!.Value as IEnumerable; - dtos.Should().NotBeNull(); - dtos.Should().HaveCount(2); - - var firstDto = dtos!.First(); - firstDto.Id.Should().Be(1); - firstDto.Name.Should().Be("llama-3.1-8b"); - firstDto.ProviderModelId.Should().Be("llama-3.1-8b-instant"); - firstDto.Capabilities.Should().NotBeNull(); - - var secondDto = dtos!.Last(); - secondDto.Id.Should().Be(2); - secondDto.Name.Should().Be("mixtral-8x7b"); - secondDto.ProviderModelId.Should().Be("mixtral-8x7b-32768"); - - _mockRepository.Verify(r => r.GetByProviderAsync(provider), Times.Once); - } - - [Fact] - public async Task GetModelsByProvider_WithEmptyProvider_ShouldReturnBadRequest() - { - // Arrange - var provider = ""; - - // Act - var result = await _controller.GetModelsByProvider(provider); - - // Assert - result.Should().BeOfType(); - var badRequestResult = result as BadRequestObjectResult; - badRequestResult!.Value.Should().Be("Provider name is required"); - - _mockRepository.Verify(r => r.GetByProviderAsync(It.IsAny()), Times.Never); - } - - [Fact] - public async Task GetModelsByProvider_WithNullProvider_ShouldReturnBadRequest() - { - // Arrange - string provider = null!; - - // Act - var result = await _controller.GetModelsByProvider(provider); - - // Assert - result.Should().BeOfType(); - var badRequestResult = result as BadRequestObjectResult; - badRequestResult!.Value.Should().Be("Provider name is required"); - - _mockRepository.Verify(r => r.GetByProviderAsync(It.IsAny()), Times.Never); - } - - [Fact] - public async Task GetModelsByProvider_WithWhitespaceProvider_ShouldReturnBadRequest() - { - // Arrange - var provider = " "; - - // Act - var result = await _controller.GetModelsByProvider(provider); - - // Assert - result.Should().BeOfType(); - var badRequestResult = result as BadRequestObjectResult; - badRequestResult!.Value.Should().Be("Provider name is required"); - - _mockRepository.Verify(r => r.GetByProviderAsync(It.IsAny()), Times.Never); - } - - [Fact] - public async Task GetModelsByProvider_WithNoModels_ShouldReturnOkWithEmptyList() - { - // Arrange - var provider = "nonexistent"; - _mockRepository.Setup(r => r.GetByProviderAsync(provider)) - .ReturnsAsync(new List()); - - // Act - var result = await _controller.GetModelsByProvider(provider); - - // Assert - result.Should().BeOfType(); - var okResult = result as OkObjectResult; - okResult.Should().NotBeNull(); - - var dtos = okResult!.Value as IEnumerable; - dtos.Should().NotBeNull(); - dtos.Should().BeEmpty(); - - _mockRepository.Verify(r => r.GetByProviderAsync(provider), Times.Once); - } - - [Fact] - public async Task GetModelsByProvider_WithModelMissingIdentifier_ShouldUseFallbackName() - { - // Arrange - var provider = "groq"; - var author = new ModelAuthor { Id = 1, Name = "Test Author" }; - var series = new ModelSeries { Id = 1, Name = "Test Series", Author = author }; - var capabilities = new ModelCapabilities { Id = 1, SupportsChat = true }; - - var models = new List - { - new Model - { - Id = 1, - Name = "test-model", - ModelSeriesId = 1, - Series = series, - ModelCapabilitiesId = 1, - Capabilities = capabilities, - IsActive = true, - Identifiers = new List - { - // Identifier for different provider - new ModelIdentifier - { - Id = 1, - ModelId = 1, - Identifier = "test-model-openai", - Provider = "openai", - IsPrimary = true - } - } - } - }; - - _mockRepository.Setup(r => r.GetByProviderAsync(provider)) - .ReturnsAsync(models); - - // Act - var result = await _controller.GetModelsByProvider(provider); - - // Assert - result.Should().BeOfType(); - var okResult = result as OkObjectResult; - var dtos = okResult!.Value as IEnumerable; - var dto = dtos!.First(); - - // Should fallback to model name when no matching provider identifier - dto.ProviderModelId.Should().Be("test-model"); - } - - [Fact] - public async Task GetModelsByProvider_WithCaseInsensitiveProviderMatch_ShouldReturnCorrectIdentifier() - { - // Arrange - var provider = "GROQ"; // Uppercase - var author = new ModelAuthor { Id = 1, Name = "Test Author" }; - var series = new ModelSeries { Id = 1, Name = "Test Series", Author = author }; - var capabilities = new ModelCapabilities { Id = 1, SupportsChat = true }; - - var models = new List - { - new Model - { - Id = 1, - Name = "test-model", - ModelSeriesId = 1, - Series = series, - ModelCapabilitiesId = 1, - Capabilities = capabilities, - IsActive = true, - Identifiers = new List - { - new ModelIdentifier - { - Id = 1, - ModelId = 1, - Identifier = "test-model-groq", - Provider = "groq", // Lowercase in DB - IsPrimary = true - } - } - } - }; - - _mockRepository.Setup(r => r.GetByProviderAsync(provider)) - .ReturnsAsync(models); - - // Act - var result = await _controller.GetModelsByProvider(provider); - - // Assert - result.Should().BeOfType(); - var okResult = result as OkObjectResult; - var dtos = okResult!.Value as IEnumerable; - var dto = dtos!.First(); - - // Should match case-insensitively - dto.ProviderModelId.Should().Be("test-model-groq"); - } - - [Fact] - public async Task GetModelsByProvider_WhenRepositoryThrows_ShouldReturn500() - { - // Arrange - var provider = "groq"; - var exception = new Exception("Database connection failed"); - - _mockRepository.Setup(r => r.GetByProviderAsync(provider)) - .ThrowsAsync(exception); - - // Act - var result = await _controller.GetModelsByProvider(provider); - - // Assert - result.Should().BeOfType(); - var objectResult = result as ObjectResult; - objectResult!.StatusCode.Should().Be(StatusCodes.Status500InternalServerError); - objectResult.Value.Should().Be("An error occurred while retrieving models"); - - // Verify logging occurred - _mockLogger.Verify( - l => l.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("Error getting models for provider")), - It.IsAny(), - It.IsAny>()), - Times.Once); - } - - [Fact] - public async Task GetModelsByProvider_WithNullCapabilities_ShouldHandleGracefully() - { - // Arrange - var provider = "groq"; - var author = new ModelAuthor { Id = 1, Name = "Test Author" }; - var series = new ModelSeries { Id = 1, Name = "Test Series", Author = author }; - - var models = new List - { - new Model - { - Id = 1, - Name = "test-model", - ModelSeriesId = 1, - Series = series, - ModelCapabilitiesId = 1, - Capabilities = null, // Null capabilities - IsActive = true, - Identifiers = new List - { - new ModelIdentifier - { - Id = 1, - ModelId = 1, - Identifier = "test-model", - Provider = "groq", - IsPrimary = true - } - } - } - }; - - _mockRepository.Setup(r => r.GetByProviderAsync(provider)) - .ReturnsAsync(models); - - // Act - var result = await _controller.GetModelsByProvider(provider); - - // Assert - result.Should().BeOfType(); - var okResult = result as OkObjectResult; - var dtos = okResult!.Value as IEnumerable; - var dto = dtos!.First(); - - dto.Capabilities.Should().BeNull(); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Controllers/ModelControllerTests.cs b/ConduitLLM.Tests/Admin/Controllers/ModelControllerTests.cs deleted file mode 100644 index d0bd5097d..000000000 --- a/ConduitLLM.Tests/Admin/Controllers/ModelControllerTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using ConduitLLM.Admin.Controllers; -using ConduitLLM.Configuration.Repositories; - -using Microsoft.Extensions.Logging; - -using Moq; - -namespace ConduitLLM.Tests.Admin.Controllers -{ - /// - /// Base unit tests for ModelController - /// Constructor tests and shared functionality - /// Other tests are split into separate files by functionality - /// - [Trait("Category", "Unit")] - [Trait("Component", "AdminController")] - public class ModelControllerTests - { - private readonly Mock _mockRepository; - private readonly Mock> _mockLogger; - - public ModelControllerTests() - { - _mockRepository = new Mock(); - _mockLogger = new Mock>(); - } - - #region Constructor Tests - - [Fact] - public void Constructor_WithValidDependencies_ShouldCreateController() - { - // Act & Assert - var controller = new ModelController(_mockRepository.Object, _mockLogger.Object); - Assert.NotNull(controller); - } - - [Fact] - public void Constructor_WithNullRepository_ShouldThrowArgumentNullException() - { - // Act & Assert - Assert.Throws(() => - new ModelController(null!, _mockLogger.Object)); - } - - [Fact] - public void Constructor_WithNullLogger_ShouldThrowArgumentNullException() - { - // Act & Assert - Assert.Throws(() => - new ModelController(_mockRepository.Object, null!)); - } - - #endregion - - /* Additional test files: - * - ModelControllerTests.GetOperations.cs - GET operations (GetAll, GetById, Search, GetIdentifiers) - * - ModelControllerTests.ProviderOperations.cs - Provider-related operations (GetModelsByProvider) - * - ModelControllerTests.CrudOperations.cs - CREATE, UPDATE, DELETE operations - */ - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Controllers/ModelProviderMappingControllerTests.AddMapping.cs b/ConduitLLM.Tests/Admin/Controllers/ModelProviderMappingControllerTests.AddMapping.cs deleted file mode 100644 index c00ceb5f1..000000000 --- a/ConduitLLM.Tests/Admin/Controllers/ModelProviderMappingControllerTests.AddMapping.cs +++ /dev/null @@ -1,121 +0,0 @@ -using ConduitLLM.Admin.Controllers; -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.Extensions; - -using FluentAssertions; - -using Microsoft.AspNetCore.Mvc; - -using Moq; - -namespace ConduitLLM.Tests.Admin.Controllers -{ - /// - /// Add mapping tests for ModelProviderMappingControllerTests - /// - public partial class ModelProviderMappingControllerTests - { - #region AddMapping Tests - - [Fact] - public async Task AddMapping_WithValidMapping_ShouldReturnCreated() - { - // Arrange - var mapping = new ModelProviderMapping - { - ModelAlias = "new-model", - ModelId = 1, - ProviderId = 1, - ProviderModelId = "gpt-4-new", - // SupportsStreaming = true - }; - - var createdMapping = new ModelProviderMapping - { - Id = 123, - ModelAlias = "new-model", - ModelId = 1, - ProviderId = 1, - ProviderModelId = "gpt-4-new", - // SupportsStreaming = true - }; - - // First call should return null (no existing mapping) - _mockService.Setup(x => x.GetMappingByModelIdAsync(1)) - .ReturnsAsync((ModelProviderMapping?)null); - - _mockService.Setup(x => x.AddMappingAsync(It.IsAny())) - .ReturnsAsync(true); - - // Second call should return the created mapping - _mockService.SetupSequence(x => x.GetMappingByModelIdAsync(1)) - .ReturnsAsync((ModelProviderMapping?)null) // First call (check existence) - .ReturnsAsync(createdMapping); // Second call (get created) - - // Act - var actionResult = await _controller.CreateMapping(mapping.ToDto()); - - // Assert - var createdResult = Assert.IsType(actionResult); - createdResult.ActionName.Should().Be(nameof(ModelProviderMappingController.GetMappingById)); - createdResult.RouteValues!["id"].Should().Be(123); - } - - [Fact] - public async Task AddMapping_WithDuplicateModelId_ShouldReturnConflict() - { - // Arrange - var mapping = new ModelProviderMapping - { - ModelAlias = "existing-model", - ModelId = 1, - ProviderId = 1, - ProviderModelId = "gpt-4" - }; - - // Mock that a mapping already exists for this model ID - _mockService.Setup(x => x.GetMappingByModelIdAsync(1)) - .ReturnsAsync(new ModelProviderMapping { Id = 999, ModelAlias = "existing-model", ModelId = 1 }); - - // Act - var actionResult = await _controller.CreateMapping(mapping.ToDto()); - - // Assert - var conflictResult = Assert.IsType(actionResult); - var errorResponse = Assert.IsType(conflictResult.Value); - errorResponse.error.ToString().Should().Contain("already exists"); - } - - [Fact] - public async Task AddMapping_WithInvalidProviderId_ShouldReturnBadRequest() - { - // Arrange - var mapping = new ModelProviderMapping - { - ModelAlias = "new-model", - ModelId = 1, - ProviderId = 999, // Invalid provider - ProviderModelId = "gpt-4" - }; - - // No existing mapping - _mockService.Setup(x => x.GetMappingByModelIdAsync(1)) - .ReturnsAsync((ModelProviderMapping?)null); - - // Add fails (e.g., invalid provider ID) - _mockService.Setup(x => x.AddMappingAsync(It.IsAny())) - .ReturnsAsync(false); - - // Act - var actionResult = await _controller.CreateMapping(mapping.ToDto()); - - // Assert - var badRequestResult = Assert.IsType(actionResult); - var errorResponse = Assert.IsType(badRequestResult.Value); - errorResponse.error.ToString().Should().Contain("Failed to create"); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Controllers/ProviderCredentialsControllerTests.cs b/ConduitLLM.Tests/Admin/Controllers/ProviderCredentialsControllerTests.cs deleted file mode 100644 index c5d5e34c6..000000000 --- a/ConduitLLM.Tests/Admin/Controllers/ProviderCredentialsControllerTests.cs +++ /dev/null @@ -1,321 +0,0 @@ -using ConduitLLM.Admin.Controllers; -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Interfaces; -using MassTransit; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit.Abstractions; -using ConduitLLM.Configuration.Interfaces; - -namespace ConduitLLM.Tests.Admin.Controllers -{ - /// - /// Unit tests for the ProviderCredentialsController class. - /// - [Trait("Category", "Unit")] - [Trait("Component", "AdminController")] - public class ProviderCredentialsControllerTests - { - private readonly Mock _mockProviderRepository; - private readonly Mock _mockKeyRepository; - private readonly Mock _mockClientFactory; - private readonly Mock _mockClient; - private readonly Mock _mockPublishEndpoint; - private readonly Mock> _mockLogger; - private readonly ProviderCredentialsController _controller; - private readonly ITestOutputHelper _output; - - public ProviderCredentialsControllerTests(ITestOutputHelper output) - { - _output = output; - _mockProviderRepository = new Mock(); - _mockKeyRepository = new Mock(); - _mockClientFactory = new Mock(); - _mockClient = new Mock(); - _mockPublishEndpoint = new Mock(); - _mockLogger = new Mock>(); - - _controller = new ProviderCredentialsController( - _mockProviderRepository.Object, - _mockKeyRepository.Object, - _mockClientFactory.Object, - _mockPublishEndpoint.Object, - _mockLogger.Object); - } - - #region Constructor Tests - - [Fact] - public void Constructor_WithNullProviderRepository_ShouldThrowArgumentNullException() - { - // Act & Assert - Assert.Throws(() => - new ProviderCredentialsController(null!, _mockKeyRepository.Object, _mockClientFactory.Object, _mockPublishEndpoint.Object, _mockLogger.Object)); - } - - [Fact] - public void Constructor_WithNullClientFactory_ShouldThrowArgumentNullException() - { - // Act & Assert - Assert.Throws(() => - new ProviderCredentialsController(_mockProviderRepository.Object, _mockKeyRepository.Object, null!, _mockPublishEndpoint.Object, _mockLogger.Object)); - } - - [Fact] - public void Constructor_WithNullLogger_ShouldThrowArgumentNullException() - { - // Act & Assert - Assert.Throws(() => - new ProviderCredentialsController(_mockProviderRepository.Object, _mockKeyRepository.Object, _mockClientFactory.Object, _mockPublishEndpoint.Object, null!)); - } - - #endregion - - #region TestProviderConnectionWithCredentials Tests - - [Fact] - public async Task TestProviderConnectionWithCredentials_WithValidCredentials_ShouldReturnSuccess() - { - // Arrange - var testRequest = new TestProviderRequest - { - ProviderType = ProviderType.OpenAI, - ApiKey = "valid-api-key", - BaseUrl = "https://api.openai.com/v1" - }; - - var mockModels = new List { "gpt-4", "gpt-3.5-turbo" }; - _mockClientFactory.Setup(x => x.CreateTestClient(It.IsAny(), It.IsAny())) - .Returns(_mockClient.Object); - _mockClient.Setup(x => x.ListModelsAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(mockModels); - - // Act - var result = await _controller.TestProviderConnectionWithCredentials(testRequest); - - // Assert - var okResult = Assert.IsType(result); - var response = okResult.Value!; - - // Use reflection to access anonymous object properties - var successProperty = response.GetType().GetProperty("Success"); - var messageProperty = response.GetType().GetProperty("Message"); - var modelCountProperty = response.GetType().GetProperty("ModelCount"); - - Assert.NotNull(successProperty); - Assert.NotNull(messageProperty); - Assert.NotNull(modelCountProperty); - - Assert.True((bool)successProperty.GetValue(response)!); - Assert.Contains("Connection successful", (string)messageProperty.GetValue(response)!); - Assert.Equal(2, (int)modelCountProperty.GetValue(response)!); - } - - [Fact] - public async Task TestProviderConnectionWithCredentials_WithInvalidApiKey_ShouldReturnFailure() - { - // Arrange - var testRequest = new TestProviderRequest - { - ProviderType = ProviderType.OpenAI, - ApiKey = "invalid-api-key", - BaseUrl = "https://api.openai.com/v1" - }; - - _mockClientFactory.Setup(x => x.CreateTestClient(It.IsAny(), It.IsAny())) - .Returns(_mockClient.Object); - _mockClient.Setup(x => x.ListModelsAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(new LLMCommunicationException("Invalid API key provided")); - - // Act - var result = await _controller.TestProviderConnectionWithCredentials(testRequest); - - // Assert - var okResult = Assert.IsType(result); - var response = okResult.Value!; - - // Use reflection to access anonymous object properties - var successProperty = response.GetType().GetProperty("Success"); - var messageProperty = response.GetType().GetProperty("Message"); - var modelCountProperty = response.GetType().GetProperty("ModelCount"); - - Assert.NotNull(successProperty); - Assert.NotNull(messageProperty); - Assert.NotNull(modelCountProperty); - - Assert.False((bool)successProperty.GetValue(response)!); - Assert.Contains("Invalid API key", (string)messageProperty.GetValue(response)!); - Assert.Equal(0, (int)modelCountProperty.GetValue(response)!); - } - - [Fact] - public async Task TestProviderConnectionWithCredentials_WithUnauthorizedError_ShouldReturnFailure() - { - // Arrange - var testRequest = new TestProviderRequest - { - ProviderType = ProviderType.OpenAI, - ApiKey = "badkey", - BaseUrl = "https://api.openai.com/v1" - }; - - _mockClientFactory.Setup(x => x.CreateTestClient(It.IsAny(), It.IsAny())) - .Returns(_mockClient.Object); - _mockClient.Setup(x => x.ListModelsAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(new LLMCommunicationException("HTTP 401: Unauthorized - Invalid API key provided")); - - // Act - var result = await _controller.TestProviderConnectionWithCredentials(testRequest); - - // Assert - var okResult = Assert.IsType(result); - var response = okResult.Value!; - - // Use reflection to access anonymous object properties - var successProperty = response.GetType().GetProperty("Success"); - var messageProperty = response.GetType().GetProperty("Message"); - var modelCountProperty = response.GetType().GetProperty("ModelCount"); - - Assert.NotNull(successProperty); - Assert.NotNull(messageProperty); - Assert.NotNull(modelCountProperty); - - Assert.False((bool)successProperty.GetValue(response)!); - Assert.Contains("Invalid API key", (string)messageProperty.GetValue(response)!); - Assert.Equal(0, (int)modelCountProperty.GetValue(response)!); - } - - [Fact] - public async Task TestProviderConnectionWithCredentials_DoesNotReturnFallbackModels() - { - // Arrange - This test verifies the fix for the original issue - var testRequest = new TestProviderRequest - { - ProviderType = ProviderType.OpenAI, - ApiKey = "fake-key-should-fail", - BaseUrl = "https://api.openai.com/v1" - }; - - _mockClientFactory.Setup(x => x.CreateTestClient(It.IsAny(), It.IsAny())) - .Returns(_mockClient.Object); - - // Mock the authentication failure that should occur with invalid key - _mockClient.Setup(x => x.ListModelsAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(new LLMCommunicationException("Authentication failed - invalid API key")); - - // Act - var result = await _controller.TestProviderConnectionWithCredentials(testRequest); - - // Assert - var okResult = Assert.IsType(result); - var response = okResult.Value!; - - // Use reflection to access anonymous object properties - var successProperty = response.GetType().GetProperty("Success"); - var messageProperty = response.GetType().GetProperty("Message"); - var modelCountProperty = response.GetType().GetProperty("ModelCount"); - - Assert.NotNull(successProperty); - Assert.NotNull(messageProperty); - Assert.NotNull(modelCountProperty); - - // Verify the connection test properly fails (no fallback models returned) - Assert.False((bool)successProperty.GetValue(response)!); - Assert.Contains("Authentication failed", (string)messageProperty.GetValue(response)!); - Assert.Equal(0, (int)modelCountProperty.GetValue(response)!); - - // Verify that ListModelsAsync was called (not bypassed by fallback) - _mockClient.Verify(x => x.ListModelsAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task TestProviderConnectionWithCredentials_WithEmptyApiKey_ShouldReturnInternalServerError() - { - // Arrange - var testRequest = new TestProviderRequest - { - ProviderType = ProviderType.OpenAI, - ApiKey = "", // Empty API key - BaseUrl = "https://api.openai.com/v1" - }; - - // Mock the client factory to throw when creating client with empty key - _mockClientFactory.Setup(x => x.CreateTestClient(It.IsAny(), It.IsAny())) - .Throws(new ArgumentException("API key is required for testing credentials")); - - // Act - var result = await _controller.TestProviderConnectionWithCredentials(testRequest); - - // Assert - Client factory exceptions result in 500 Internal Server Error - var statusResult = Assert.IsType(result); - Assert.Equal(500, statusResult.StatusCode); - Assert.Equal("An unexpected error occurred.", statusResult.Value); - } - - [Fact] - public async Task TestProviderConnectionWithCredentials_WithNullApiKey_ShouldReturnInternalServerError() - { - // Arrange - var testRequest = new TestProviderRequest - { - ProviderType = ProviderType.OpenAI, - ApiKey = null, // Null API key - BaseUrl = "https://api.openai.com/v1" - }; - - // Mock the client factory to throw when creating client with null key - _mockClientFactory.Setup(x => x.CreateTestClient(It.IsAny(), It.IsAny())) - .Throws(new ArgumentException("API key is required for testing credentials")); - - // Act - var result = await _controller.TestProviderConnectionWithCredentials(testRequest); - - // Assert - Client factory exceptions result in 500 Internal Server Error - var statusResult = Assert.IsType(result); - Assert.Equal(500, statusResult.StatusCode); - Assert.Equal("An unexpected error occurred.", statusResult.Value); - } - - [Fact] - public async Task TestProviderConnectionWithCredentials_WithGenericException_ShouldReturnFailure() - { - // Arrange - var testRequest = new TestProviderRequest - { - ProviderType = ProviderType.OpenAI, - ApiKey = "test-key", - BaseUrl = "https://api.openai.com/v1" - }; - - _mockClientFactory.Setup(x => x.CreateTestClient(It.IsAny(), It.IsAny())) - .Returns(_mockClient.Object); - _mockClient.Setup(x => x.ListModelsAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(new Exception("Network timeout")); - - // Act - var result = await _controller.TestProviderConnectionWithCredentials(testRequest); - - // Assert - var okResult = Assert.IsType(result); - var response = okResult.Value!; - - // Use reflection to access anonymous object properties - var successProperty = response.GetType().GetProperty("Success"); - var messageProperty = response.GetType().GetProperty("Message"); - var modelCountProperty = response.GetType().GetProperty("ModelCount"); - - Assert.NotNull(successProperty); - Assert.NotNull(messageProperty); - Assert.NotNull(modelCountProperty); - - Assert.False((bool)successProperty.GetValue(response)!); - Assert.Contains("Network timeout", (string)messageProperty.GetValue(response)!); - Assert.Equal(0, (int)modelCountProperty.GetValue(response)!); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Integration/ModelCostIntegrationTests.Create.cs b/ConduitLLM.Tests/Admin/Integration/ModelCostIntegrationTests.Create.cs deleted file mode 100644 index c8bed2a1a..000000000 --- a/ConduitLLM.Tests/Admin/Integration/ModelCostIntegrationTests.Create.cs +++ /dev/null @@ -1,82 +0,0 @@ -using ConduitLLM.Configuration.DTOs; - -using FluentAssertions; - -using Microsoft.AspNetCore.Mvc; - -namespace ConduitLLM.Tests.Admin.Integration -{ - /// - /// Create model cost tests for ModelCostIntegrationTests - /// - public partial class ModelCostIntegrationTests - { - #region Create Model Cost Tests - - [Fact] - public async Task CreateModelCost_WithMappings_ShouldCreateAndAssociate() - { - // Arrange - var providerId = await SetupTestDataAsync(); - var mappings = await _modelMappingRepository.GetAllAsync(); - var mappingIds = mappings.Select(m => m.Id).Take(2).ToList(); // Use first 2 mappings - - var createDto = new CreateModelCostDto - { - CostName = "GPT-4 Pricing", - InputCostPerMillionTokens = 30.00m, - OutputCostPerMillionTokens = 60.00m, - ModelProviderMappingIds = mappingIds - }; - - // Act - var result = await _controller.CreateModelCost(createDto); - - // Assert - var createdResult = Assert.IsType(result); - var createdCost = Assert.IsType(createdResult.Value); - - createdCost.CostName.Should().Be("GPT-4 Pricing"); - createdCost.AssociatedModelAliases.Should().HaveCount(2); - createdCost.AssociatedModelAliases.Should().Contain(new[] { "gpt-4", "gpt-3.5-turbo" }); - - // Verify in database - var dbCost = await _modelCostRepository.GetByIdAsync(createdCost.Id); - dbCost.Should().NotBeNull(); - dbCost!.ModelCostMappings.Should().HaveCount(2); - } - - [Fact] - public async Task CreateModelCost_DuplicateName_ShouldReturnBadRequest() - { - // Arrange - await SetupTestDataAsync(); - - // Create first cost - var firstCost = new CreateModelCostDto - { - CostName = "Standard Pricing", - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m - }; - await _controller.CreateModelCost(firstCost); - - // Try to create duplicate - var duplicateCost = new CreateModelCostDto - { - CostName = "Standard Pricing", - InputCostPerMillionTokens = 15.00m, - OutputCostPerMillionTokens = 25.00m - }; - - // Act - var result = await _controller.CreateModelCost(duplicateCost); - - // Assert - var badRequestResult = Assert.IsType(result); - badRequestResult.Value.Should().Be("A model cost with name 'Standard Pricing' already exists"); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Integration/ModelCostIntegrationTests.Get.cs b/ConduitLLM.Tests/Admin/Integration/ModelCostIntegrationTests.Get.cs deleted file mode 100644 index e3c8c79c4..000000000 --- a/ConduitLLM.Tests/Admin/Integration/ModelCostIntegrationTests.Get.cs +++ /dev/null @@ -1,101 +0,0 @@ -using ConduitLLM.Configuration.DTOs; - -using FluentAssertions; - -using Microsoft.AspNetCore.Mvc; - -namespace ConduitLLM.Tests.Admin.Integration -{ - /// - /// Get model cost tests for ModelCostIntegrationTests - /// - public partial class ModelCostIntegrationTests - { - #region Get Model Cost Tests - - [Fact] - public async Task GetModelCostById_WithMappings_ShouldReturnAssociatedAliases() - { - // Arrange - var providerId = await SetupTestDataAsync(); - var mappings = await _modelMappingRepository.GetAllAsync(); - var mappingIds = mappings.Select(m => m.Id).ToList(); - - var createDto = new CreateModelCostDto - { - CostName = "Test Cost with Associations", - InputCostPerMillionTokens = 25.00m, - OutputCostPerMillionTokens = 50.00m, - ModelProviderMappingIds = mappingIds - }; - - var createResult = await _controller.CreateModelCost(createDto); - var createdCost = (createResult as CreatedAtActionResult)?.Value as ModelCostDto; - - // Act - var getResult = await _controller.GetModelCostById(createdCost!.Id); - - // Assert - var okResult = Assert.IsType(getResult); - var retrievedCost = Assert.IsType(okResult.Value); - - retrievedCost.CostName.Should().Be("Test Cost with Associations"); - retrievedCost.AssociatedModelAliases.Should().HaveCount(3); - retrievedCost.AssociatedModelAliases.Should().Contain(new[] { "gpt-4", "gpt-3.5-turbo", "text-embedding-ada-002" }); - } - - [Fact] - public async Task GetAllModelCosts_ShouldReturnAllWithAssociations() - { - // Arrange - var providerId = await SetupTestDataAsync(); - var mappings = await _modelMappingRepository.GetAllAsync(); - var mappingsList = mappings.OrderBy(m => m.ModelAlias).ToList(); // Order for predictability - - // Get specific mappings by alias - var gpt4Mapping = mappingsList.First(m => m.ModelAlias == "gpt-4"); - var gpt35Mapping = mappingsList.First(m => m.ModelAlias == "gpt-3.5-turbo"); - var embeddingMapping = mappingsList.First(m => m.ModelAlias == "text-embedding-ada-002"); - - // Create multiple costs with different mappings - var cost1 = new CreateModelCostDto - { - CostName = "Cost 1", - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m, - ModelProviderMappingIds = new List { gpt4Mapping.Id } - }; - - var cost2 = new CreateModelCostDto - { - CostName = "Cost 2", - InputCostPerMillionTokens = 15.00m, - OutputCostPerMillionTokens = 30.00m, - ModelProviderMappingIds = new List { gpt35Mapping.Id, embeddingMapping.Id } - }; - - await _controller.CreateModelCost(cost1); - await _controller.CreateModelCost(cost2); - - // Act - var result = await _controller.GetAllModelCosts(); - - // Assert - var okResult = Assert.IsType(result); - var costs = Assert.IsAssignableFrom>(okResult.Value); - var costList = costs.ToList(); - - costList.Should().HaveCount(2); - - var firstCost = costList.First(c => c.CostName == "Cost 1"); - firstCost.AssociatedModelAliases.Should().HaveCount(1); - firstCost.AssociatedModelAliases.Should().Contain("gpt-4"); - - var secondCost = costList.First(c => c.CostName == "Cost 2"); - secondCost.AssociatedModelAliases.Should().HaveCount(2); - secondCost.AssociatedModelAliases.Should().Contain(new[] { "gpt-3.5-turbo", "text-embedding-ada-002" }); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Integration/ModelCostIntegrationTests.Update.cs b/ConduitLLM.Tests/Admin/Integration/ModelCostIntegrationTests.Update.cs deleted file mode 100644 index 8b94a06d2..000000000 --- a/ConduitLLM.Tests/Admin/Integration/ModelCostIntegrationTests.Update.cs +++ /dev/null @@ -1,113 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.DTOs; - -using FluentAssertions; - -using Microsoft.AspNetCore.Mvc; - -namespace ConduitLLM.Tests.Admin.Integration -{ - /// - /// Update model cost tests for ModelCostIntegrationTests - /// - public partial class ModelCostIntegrationTests - { - #region Update Model Cost Tests - - [Fact] - public async Task UpdateModelCost_ChangeMappings_ShouldUpdateCorrectly() - { - // Arrange - var providerId = await SetupTestDataAsync(); - var allMappings = await _modelMappingRepository.GetAllAsync(); - var initialMappingIds = allMappings.Select(m => m.Id).Take(2).ToList(); - var newMappingIds = allMappings.Select(m => m.Id).Skip(1).Take(2).ToList(); - - // Create initial cost with mappings - var createDto = new CreateModelCostDto - { - CostName = "Test Pricing", - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m, - ModelProviderMappingIds = initialMappingIds - }; - - var createResult = await _controller.CreateModelCost(createDto); - var createdCost = (createResult as CreatedAtActionResult)?.Value as ModelCostDto; - createdCost.Should().NotBeNull(); - - // Update with different mappings - var updateDto = new UpdateModelCostDto - { - Id = createdCost!.Id, - CostName = "Updated Pricing", - InputCostPerMillionTokens = 15.00m, - OutputCostPerMillionTokens = 25.00m, - ModelProviderMappingIds = newMappingIds - }; - - // Act - var updateResult = await _controller.UpdateModelCost(createdCost.Id, updateDto); - - // Assert - Assert.IsType(updateResult); - - // Verify updated mappings - var updatedCost = await _modelCostRepository.GetByIdAsync(createdCost.Id); - updatedCost.Should().NotBeNull(); - updatedCost!.CostName.Should().Be("Updated Pricing"); - updatedCost.ModelCostMappings.Should().HaveCount(2); - - var actualMappingIds = updatedCost.ModelCostMappings.Select(m => m.ModelProviderMappingId).ToList(); - actualMappingIds.Should().BeEquivalentTo(newMappingIds); - } - - [Fact] - public async Task UpdateModelCost_RemoveAllMappings_ShouldClearAssociations() - { - // Arrange - var providerId = await SetupTestDataAsync(); - var mappings = await _modelMappingRepository.GetAllAsync(); - var mappingIds = mappings.Select(m => m.Id).ToList(); - - // Create cost with mappings - var createDto = new CreateModelCostDto - { - CostName = "Test Cost", - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m, - ModelProviderMappingIds = mappingIds - }; - - var createResult = await _controller.CreateModelCost(createDto); - var createdCost = (createResult as CreatedAtActionResult)?.Value as ModelCostDto; - - // Update to remove all mappings - var updateDto = new UpdateModelCostDto - { - Id = createdCost!.Id, - CostName = createdCost.CostName, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m, - ModelProviderMappingIds = new List() // Empty list - }; - - // Act - var updateResult = await _controller.UpdateModelCost(createdCost.Id, updateDto); - - // Assert - Assert.IsType(updateResult); - - // Verify mappings removed - using (var verifyContext = new ConduitDbContext(_dbContextOptions)) - { - var dbMappings = verifyContext.ModelCostMappings - .Where(m => m.ModelCostId == createdCost.Id) - .ToList(); - dbMappings.Should().BeEmpty(); - } - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Mapping.cs b/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Mapping.cs deleted file mode 100644 index 92c7526ca..000000000 --- a/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Mapping.cs +++ /dev/null @@ -1,403 +0,0 @@ -using ConduitLLM.Admin.Models.ModelCapabilities; - -using FluentAssertions; - -namespace ConduitLLM.Tests.Admin.Models.ModelCapabilities -{ - /// - /// Tests for entity-to-DTO and DTO-to-entity mapping logic. - /// These tests ensure data is correctly transformed between layers. - /// - public partial class CapabilitiesDtoTests - { - [Fact] - public void Should_Map_Entity_To_ModelCapabilitiesDto_Correctly() - { - // Arrange - var entity = new ConduitLLM.Configuration.Entities.ModelCapabilities - { - Id = 10, - SupportsChat = true, - 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 - var dto = MapEntityToDto(entity); - - // Assert - dto.Should().NotBeNull(); - dto.Id.Should().Be(entity.Id); - dto.SupportsChat.Should().Be(entity.SupportsChat); - 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] - public void Should_Map_CreateCapabilitiesDto_To_Entity() - { - // Arrange - var createDto = new CreateCapabilitiesDto - { - SupportsChat = true, - 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 - var entity = new ConduitLLM.Configuration.Entities.ModelCapabilities - { - SupportsChat = createDto.SupportsChat, - 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 - entity.SupportsChat.Should().BeTrue(); - entity.SupportsVision.Should().BeFalse(); - entity.SupportsFunctionCalling.Should().BeTrue(); - entity.SupportsStreaming.Should().BeTrue(); - 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] - public void Should_Apply_UpdateCapabilitiesDto_To_Entity_With_Partial_Updates() - { - // Arrange - var existingEntity = new ConduitLLM.Configuration.Entities.ModelCapabilities - { - Id = 5, - SupportsChat = true, - 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 - { - Id = 5, - SupportsChat = null, // Don't update - SupportsVision = true, // Enable vision - SupportsFunctionCalling = true, // Enable function calling - SupportsStreaming = null, // Don't update - 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 - if (updateDto.SupportsChat.HasValue) - existingEntity.SupportsChat = updateDto.SupportsChat.Value; - if (updateDto.SupportsVision.HasValue) - existingEntity.SupportsVision = updateDto.SupportsVision.Value; - if (updateDto.SupportsFunctionCalling.HasValue) - existingEntity.SupportsFunctionCalling = updateDto.SupportsFunctionCalling.Value; - if (updateDto.SupportsStreaming.HasValue) - existingEntity.SupportsStreaming = updateDto.SupportsStreaming.Value; - if (updateDto.MaxTokens.HasValue) - existingEntity.MaxTokens = updateDto.MaxTokens.Value; - if (updateDto.MinTokens.HasValue) - 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 - existingEntity.SupportsVision.Should().BeTrue(); // Updated - existingEntity.SupportsFunctionCalling.Should().BeTrue(); // Updated - existingEntity.SupportsStreaming.Should().BeTrue(); // Unchanged - 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] - public void Should_Handle_Clearing_String_Properties_With_Empty_String() - { - // Arrange - 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 - }; - - 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] - public void Should_Not_Update_String_Properties_When_Null() - { - // Arrange - 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 - }; - - 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] - public void Should_Map_Entity_With_All_Capabilities_To_Dto() - { - // Arrange - Entity with all capabilities enabled - var entity = new ConduitLLM.Configuration.Entities.ModelCapabilities - { - Id = 99, - SupportsChat = true, - 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 - var dto = MapEntityToDto(entity); - - // Assert - All capabilities should be true - dto.SupportsChat.Should().BeTrue(); - 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(); - dto.MaxTokens.Should().Be(int.MaxValue); - } - - [Fact] - public void Should_Map_Entity_With_No_Capabilities_To_Dto() - { - // Arrange - Entity with all capabilities disabled - var entity = new ConduitLLM.Configuration.Entities.ModelCapabilities - { - Id = 1, - SupportsChat = false, - 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 - var dto = MapEntityToDto(entity); - - // Assert - All capabilities should be false - dto.SupportsChat.Should().BeFalse(); - 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(0); - } - - [Fact] - public void Should_Default_MinTokens_To_One_When_Creating_Entity() - { - // Arrange - var createDto = new CreateCapabilitiesDto - { - SupportsChat = true, - MaxTokens = 4096 - // MinTokens not explicitly set, uses default - }; - - // Act - simulate controller logic with default - var entity = new ConduitLLM.Configuration.Entities.ModelCapabilities - { - SupportsChat = createDto.SupportsChat, - MaxTokens = createDto.MaxTokens, - MinTokens = createDto.MinTokens, // Should be 1 by default - TokenizerType = createDto.TokenizerType - }; - - // Assert - entity.MinTokens.Should().Be(1); - } - - // Helper method that mirrors controller mapping logic - private static ModelCapabilitiesDto MapEntityToDto(ConduitLLM.Configuration.Entities.ModelCapabilities entity) - { - return new ModelCapabilitiesDto - { - Id = entity.Id, - SupportsChat = entity.SupportsChat, - 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 - }; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Serialization.cs b/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Serialization.cs deleted file mode 100644 index 1c8d81d44..000000000 --- a/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Serialization.cs +++ /dev/null @@ -1,325 +0,0 @@ -using System.Text.Json; - -using ConduitLLM.Admin.Models.ModelCapabilities; - -using FluentAssertions; - -namespace ConduitLLM.Tests.Admin.Models.ModelCapabilities -{ - /// - /// Tests for CapabilitiesDto serialization and deserialization behavior. - /// These tests ensure API contract stability and catch breaking changes. - /// - public partial class CapabilitiesDtoTests - { - [Fact] - public void ModelCapabilitiesDto_Should_Serialize_All_Boolean_Flags() - { - // Arrange - var dto = new ModelCapabilitiesDto - { - Id = 1, - SupportsChat = true, - 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 - var json = JsonSerializer.Serialize(dto); - var deserialized = JsonSerializer.Deserialize(json); - - // Assert - deserialized.Should().NotBeNull(); - deserialized!.SupportsChat.Should().BeTrue(); - 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(); - } - - [Fact] - public void ModelCapabilitiesDto_Should_Serialize_Token_Limits() - { - // Arrange - var testCases = new[] - { - (min: 1, max: 4096), - (min: 0, max: 128000), - (min: 100, max: 200000), - (min: 1, max: int.MaxValue), - (min: 0, max: 0) // Edge case - }; - - foreach (var (min, max) in testCases) - { - // Arrange - var dto = new ModelCapabilitiesDto - { - Id = 1, - MinTokens = min, - MaxTokens = max, - TokenizerType = TokenizerType.BPE - }; - - // Act - var json = JsonSerializer.Serialize(dto); - var deserialized = JsonSerializer.Deserialize(json); - - // Assert - deserialized.Should().NotBeNull(); - deserialized!.MinTokens.Should().Be(min); - deserialized!.MaxTokens.Should().Be(max); - } - } - - [Fact] - public void CapabilitiesDto_Should_Handle_Null_String_Properties() - { - // Arrange - var dto = new CapabilitiesDto - { - Id = 1, - SupportsChat = true, - MaxTokens = 4096, - MinTokens = 1, - TokenizerType = TokenizerType.BPE, - SupportedVoices = null, - SupportedLanguages = null, - SupportedFormats = null - }; - - // Act - var json = JsonSerializer.Serialize(dto); - var deserialized = JsonSerializer.Deserialize(json); - - // Assert - deserialized.Should().NotBeNull(); - deserialized!.SupportedVoices.Should().BeNull(); - deserialized.SupportedLanguages.Should().BeNull(); - deserialized.SupportedFormats.Should().BeNull(); - } - - [Fact] - public void CapabilitiesDto_Should_Serialize_Comma_Separated_Values() - { - // Arrange - 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 - }; - - // Act - var json = JsonSerializer.Serialize(dto); - var deserialized = JsonSerializer.Deserialize(json); - - // 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] - public void CreateCapabilitiesDto_Should_Serialize_Without_Id() - { - // Arrange - var dto = new CreateCapabilitiesDto - { - SupportsChat = true, - SupportsVision = true, - MaxTokens = 128000, - MinTokens = 1, - TokenizerType = TokenizerType.Cl100KBase - }; - - // Act - var json = JsonSerializer.Serialize(dto); - - // Assert - json.Should().NotContain("\"Id\""); - json.Should().Contain("\"SupportsChat\":true"); - json.Should().Contain("\"SupportsVision\":true"); - json.Should().Contain("\"MaxTokens\":128000"); - json.Should().Contain("\"MinTokens\":1"); - } - - [Fact] - public void UpdateCapabilitiesDto_Should_Handle_Partial_Updates() - { - // Arrange - var dto = new UpdateCapabilitiesDto - { - Id = 5, - SupportsChat = true, - SupportsVision = null, // Don't update - MaxTokens = 200000, // Update - MinTokens = null, // Don't update - TokenizerType = null // Don't update - }; - - // Act - var json = JsonSerializer.Serialize(dto); - var deserialized = JsonSerializer.Deserialize(json); - - // Assert - deserialized.Should().NotBeNull(); - deserialized!.Id.Should().Be(5); - deserialized.SupportsChat.Should().BeTrue(); - deserialized.SupportsVision.Should().BeNull(); - deserialized.MaxTokens.Should().Be(200000); - deserialized.MinTokens.Should().BeNull(); - deserialized.TokenizerType.Should().BeNull(); - } - - [Theory] - [InlineData(TokenizerType.Cl100KBase)] - [InlineData(TokenizerType.P50KBase)] - [InlineData(TokenizerType.P50KEdit)] - [InlineData(TokenizerType.R50KBase)] - [InlineData(TokenizerType.Claude)] - [InlineData(TokenizerType.O200KBase)] - [InlineData(TokenizerType.LLaMA)] - [InlineData(TokenizerType.BPE)] - public void CapabilitiesDto_Should_Serialize_All_TokenizerTypes(TokenizerType tokenizerType) - { - // Arrange - var dto = new CapabilitiesDto - { - Id = 1, - TokenizerType = tokenizerType, - MaxTokens = 4096, - MinTokens = 1 - }; - - // Act - var json = JsonSerializer.Serialize(dto); - var deserialized = JsonSerializer.Deserialize(json); - - // Assert - deserialized.Should().NotBeNull(); - deserialized!.TokenizerType.Should().Be(tokenizerType); - } - - [Fact] - public void CapabilitiesDto_Should_Handle_Empty_String_Properties() - { - // Arrange - var dto = new CapabilitiesDto - { - Id = 1, - SupportedVoices = "", - SupportedLanguages = "", - SupportedFormats = "", - TokenizerType = TokenizerType.BPE, - MaxTokens = 4096, - MinTokens = 1 - }; - - // Act - var json = JsonSerializer.Serialize(dto); - var deserialized = JsonSerializer.Deserialize(json); - - // Assert - deserialized.Should().NotBeNull(); - deserialized!.SupportedVoices.Should().BeEmpty(); - deserialized.SupportedLanguages.Should().BeEmpty(); - deserialized.SupportedFormats.Should().BeEmpty(); - } - - [Fact] - public void ModelCapabilitiesDto_Should_Be_Assignable_To_CapabilitiesDto() - { - // Arrange - var modelCapDto = new ModelCapabilitiesDto - { - Id = 1, - SupportsChat = true, - MaxTokens = 4096, - MinTokens = 1, - TokenizerType = TokenizerType.Cl100KBase - }; - - // Act - Should be able to treat as base type - CapabilitiesDto baseDto = modelCapDto; - - // Assert - baseDto.Should().NotBeNull(); - baseDto.Id.Should().Be(1); - baseDto.SupportsChat.Should().BeTrue(); - baseDto.Should().BeOfType(); - } - - [Fact] - public void CapabilitiesDto_Should_Handle_Unicode_In_Supported_Languages() - { - // Arrange - var dto = new CapabilitiesDto - { - Id = 1, - SupportedLanguages = "中文,日本語,한국어,العربية,עברית,русский", - TokenizerType = TokenizerType.BPE, - MaxTokens = 4096, - MinTokens = 1 - }; - - // Act - var json = JsonSerializer.Serialize(dto); - var deserialized = JsonSerializer.Deserialize(json); - - // Assert - deserialized.Should().NotBeNull(); - deserialized!.SupportedLanguages.Should().Be("中文,日本語,한국어,العربية,עברית,русский"); - } - - [Fact] - public void CapabilitiesDto_Should_Preserve_Property_Names_In_Json() - { - // Arrange - var dto = new CapabilitiesDto - { - Id = 1, - SupportsChat = true, - SupportsVision = false, - MaxTokens = 4096, - MinTokens = 1, - TokenizerType = TokenizerType.BPE - }; - - // Act - var json = JsonSerializer.Serialize(dto); - - // Assert - ensure JSON property names match expectations for API compatibility - json.Should().Contain("\"Id\":"); - json.Should().Contain("\"SupportsChat\":"); - json.Should().Contain("\"SupportsVision\":"); - json.Should().Contain("\"MaxTokens\":"); - json.Should().Contain("\"MinTokens\":"); - json.Should().Contain("\"TokenizerType\":"); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Validation.cs b/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Validation.cs deleted file mode 100644 index f8f3abe73..000000000 --- a/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Validation.cs +++ /dev/null @@ -1,360 +0,0 @@ -using ConduitLLM.Admin.Models.ModelCapabilities; - -using FluentAssertions; - -namespace ConduitLLM.Tests.Admin.Models.ModelCapabilities -{ - /// - /// Tests for CapabilitiesDto validation rules and business constraints. - /// These tests ensure DTOs enforce proper data integrity. - /// - public partial class CapabilitiesDtoTests - { - [Fact] - public void CreateCapabilitiesDto_Should_Have_Default_Values() - { - // Act - var dto = new CreateCapabilitiesDto(); - - // Assert - all booleans default to false - dto.SupportsChat.Should().BeFalse(); - 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] - public void CapabilitiesDto_Should_Validate_Token_Range() - { - // Arrange - var dto = new CapabilitiesDto - { - Id = 1, - MinTokens = 100, - MaxTokens = 50, // Max less than min! - TokenizerType = TokenizerType.BPE - }; - - // Act & Assert - // DTO allows this, but business logic should validate - dto.MinTokens.Should().Be(100); - dto.MaxTokens.Should().Be(50); - - // Business validation - var isInvalid = dto.MinTokens > dto.MaxTokens; - isInvalid.Should().BeTrue("Controller should validate min <= max"); - } - - [Theory] - // Note: 0 is actually valid (some models might have 0 minimum) - // [InlineData(0)] - removed as 0 is not invalid - [InlineData(-1)] - [InlineData(-100)] - [InlineData(int.MinValue)] - public void CapabilitiesDto_Should_Accept_Invalid_Token_Values_But_Controller_Should_Validate(int tokens) - { - // Arrange & Act - var dto = new CapabilitiesDto - { - Id = 1, - MinTokens = tokens, - MaxTokens = tokens, - TokenizerType = TokenizerType.BPE - }; - - // Assert - // DTO accepts any int, but controller should validate - dto.MinTokens.Should().Be(tokens); - dto.MaxTokens.Should().Be(tokens); - - // Business rule check for MinTokens - var isInvalidMin = tokens < 0; - isInvalidMin.Should().BeTrue("Controller should reject negative token counts"); - } - - [Fact] - public void UpdateCapabilitiesDto_Should_Allow_Partial_Updates() - { - // Arrange & Act - var dto = new UpdateCapabilitiesDto - { - Id = 5, - SupportsChat = true, // Update - SupportsVision = null, // Don't update - SupportsFunctionCalling = false, // Update - SupportsStreaming = null, // Don't update - 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" - dto.Id.Should().Be(5); - dto.SupportsChat.Should().BeTrue(); - dto.SupportsVision.Should().BeNull(); - dto.SupportsFunctionCalling.Should().BeFalse(); - dto.SupportsStreaming.Should().BeNull(); - 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] - public void UpdateCapabilitiesDto_Should_Allow_All_Null_For_No_Updates() - { - // Arrange & Act - var dto = new UpdateCapabilitiesDto - { - Id = 1, - SupportsChat = null, - 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) - dto.Id.Should().Be(1); - dto.SupportsChat.Should().BeNull(); - dto.MaxTokens.Should().BeNull(); - dto.TokenizerType.Should().BeNull(); - } - - [Fact] - public void CapabilitiesDto_Should_Validate_Conflicting_Capabilities() - { - // Arrange - var dto = new CapabilitiesDto - { - Id = 1, - SupportsChat = false, // Doesn't support chat - SupportsFunctionCalling = true, // But supports function calling? - SupportsStreaming = true, // And streaming? - MaxTokens = 4096, - MinTokens = 1, - TokenizerType = TokenizerType.BPE - }; - - // Act & Assert - // DTO allows this, but business logic might validate - dto.SupportsChat.Should().BeFalse(); - dto.SupportsFunctionCalling.Should().BeTrue(); - dto.SupportsStreaming.Should().BeTrue(); - - // Business validation - function calling typically requires chat - var isInconsistent = !dto.SupportsChat && dto.SupportsFunctionCalling; - 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() - { - // Arrange - var dto = new CreateCapabilitiesDto - { - SupportsChat = true, - MaxTokens = 4096 - // MinTokens not specified - }; - - // Act & Assert - dto.MinTokens.Should().Be(1, "MinTokens should default to 1 for new capabilities"); - } - - [Fact] - public void CapabilitiesDto_Should_Validate_Embedding_Model_Constraints() - { - // Arrange - var dto = new CapabilitiesDto - { - Id = 1, - SupportsEmbeddings = true, - SupportsChat = false, // Embedding models typically don't chat - SupportsVision = false, - SupportsFunctionCalling = false, - MaxTokens = 8192, // Embedding models have token limits - MinTokens = 1, - TokenizerType = TokenizerType.Cl100KBase - }; - - // Act & Assert - dto.SupportsEmbeddings.Should().BeTrue(); - dto.SupportsChat.Should().BeFalse(); - - // Business validation - embedding models shouldn't have chat features - var isValidEmbeddingModel = dto.SupportsEmbeddings && - !dto.SupportsChat && - !dto.SupportsFunctionCalling; - isValidEmbeddingModel.Should().BeTrue("Embedding models typically don't support chat"); - } - - [Fact] - public void CapabilitiesDto_Should_Validate_Audio_Model_Constraints() - { - // Arrange - 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"); - } - - [Fact] - public void UpdateCapabilitiesDto_Should_Clear_Lists_With_Empty_String() - { - // Arrange - 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] - public void CapabilitiesDto_Should_Handle_All_Capabilities_Enabled() - { - // Arrange - Model that supports everything - var dto = new CapabilitiesDto - { - Id = 1, - SupportsChat = true, - 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 - }; - - // Act & Assert - var allCapabilities = new[] - { - dto.SupportsChat, - dto.SupportsVision, - dto.SupportsFunctionCalling, - dto.SupportsStreaming, - dto.SupportsAudioTranscription, - dto.SupportsTextToSpeech, - dto.SupportsRealtimeAudio, - dto.SupportsImageGeneration, - dto.SupportsVideoGeneration, - dto.SupportsEmbeddings - }; - - allCapabilities.Should().AllSatisfy(cap => cap.Should().BeTrue()); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Models/Models/ModelDtoTests.Mapping.cs b/ConduitLLM.Tests/Admin/Models/Models/ModelDtoTests.Mapping.cs deleted file mode 100644 index fc914f37a..000000000 --- a/ConduitLLM.Tests/Admin/Models/Models/ModelDtoTests.Mapping.cs +++ /dev/null @@ -1,297 +0,0 @@ -using ConduitLLM.Admin.Models.Models; -using ConduitLLM.Admin.Models.ModelCapabilities; -using ConduitLLM.Configuration.Entities; -using FluentAssertions; - -namespace ConduitLLM.Tests.Admin.Models.Models -{ - /// - /// Tests for entity-to-DTO and DTO-to-entity mapping logic. - /// These tests ensure data is correctly transformed between layers. - /// - public partial class ModelDtoTests - { - [Fact] - public void Should_Map_Entity_To_ModelDto_Correctly() - { - // Arrange - var entity = new Model - { - Id = 42, - Name = "gpt-4-turbo", - ModelSeriesId = 5, - ModelCapabilitiesId = 10, - Capabilities = new ConduitLLM.Configuration.Entities.ModelCapabilities - { - Id = 10, - SupportsChat = true, - SupportsVision = true, - SupportsFunctionCalling = true, - SupportsStreaming = true, - 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), - UpdatedAt = new DateTime(2024, 1, 20, 14, 45, 0, DateTimeKind.Utc) - }; - - // Act - simulate the mapping logic from controller - var dto = MapEntityToDto(entity); - - // Assert - dto.Should().NotBeNull(); - dto.Id.Should().Be(entity.Id); - dto.Name.Should().Be(entity.Name); - dto.ModelSeriesId.Should().Be(entity.ModelSeriesId); - dto.ModelCapabilitiesId.Should().Be(entity.ModelCapabilitiesId); - dto.IsActive.Should().Be(entity.IsActive); - dto.CreatedAt.Should().Be(entity.CreatedAt); - dto.UpdatedAt.Should().Be(entity.UpdatedAt); - - // Capabilities mapping - dto.Capabilities.Should().NotBeNull(); - dto.Capabilities!.Id.Should().Be(entity.Capabilities.Id); - dto.Capabilities.SupportsChat.Should().Be(entity.Capabilities.SupportsChat); - dto.Capabilities.SupportsVision.Should().Be(entity.Capabilities.SupportsVision); - dto.Capabilities.MaxTokens.Should().Be(entity.Capabilities.MaxTokens); - dto.Capabilities.MinTokens.Should().Be(entity.Capabilities.MinTokens); - dto.Capabilities.TokenizerType.Should().Be(entity.Capabilities.TokenizerType); - } - - [Fact] - public void Should_Map_Entity_With_Null_Capabilities_To_Dto() - { - // Arrange - var entity = new Model - { - Id = 1, - Name = "test-model", - ModelSeriesId = 1, - ModelCapabilitiesId = 1, - Capabilities = null, // No capabilities loaded - IsActive = true, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }; - - // Act - var dto = MapEntityToDto(entity); - - // Assert - dto.Should().NotBeNull(); - dto.Capabilities.Should().BeNull(); - dto.ModelCapabilitiesId.Should().Be(1); // ID still set even if object not loaded - } - - [Fact] - public void Should_Map_CreateModelDto_To_Entity() - { - // Arrange - var createDto = new CreateModelDto - { - Name = "llama-3.1-405b", - ModelSeriesId = 3, - ModelCapabilitiesId = 7, - IsActive = true - }; - - // Act - simulate controller logic - var entity = new Model - { - Name = createDto.Name, - ModelSeriesId = createDto.ModelSeriesId, - ModelCapabilitiesId = createDto.ModelCapabilitiesId, - IsActive = createDto.IsActive ?? true, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }; - - // Assert - entity.Name.Should().Be(createDto.Name); - entity.ModelSeriesId.Should().Be(createDto.ModelSeriesId); - entity.ModelCapabilitiesId.Should().Be(createDto.ModelCapabilitiesId); - entity.IsActive.Should().BeTrue(); - } - - [Fact] - public void Should_Apply_UpdateModelDto_To_Entity_With_Partial_Updates() - { - // Arrange - var existingEntity = new Model - { - Id = 42, - Name = "original-name", - ModelSeriesId = 1, - ModelCapabilitiesId = 5, - IsActive = true, - CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), - UpdatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc) - }; - - var updateDto = new UpdateModelDto - { - Id = 42, - Name = "updated-name", // Update name - ModelSeriesId = null, // Don't update series - ModelCapabilitiesId = 10, // Update capabilities - IsActive = null // Don't update status - }; - - // Act - simulate controller update logic - if (!string.IsNullOrEmpty(updateDto.Name)) - existingEntity.Name = updateDto.Name; - if (updateDto.ModelSeriesId.HasValue) - existingEntity.ModelSeriesId = updateDto.ModelSeriesId.Value; - if (updateDto.ModelCapabilitiesId.HasValue) - existingEntity.ModelCapabilitiesId = updateDto.ModelCapabilitiesId.Value; - if (updateDto.IsActive.HasValue) - existingEntity.IsActive = updateDto.IsActive.Value; - existingEntity.UpdatedAt = DateTime.UtcNow; - - // Assert - existingEntity.Name.Should().Be("updated-name"); - existingEntity.ModelSeriesId.Should().Be(1); // Unchanged - existingEntity.ModelCapabilitiesId.Should().Be(10); // Updated - existingEntity.IsActive.Should().BeTrue(); // Unchanged - } - - [Fact] - public void Should_Map_Entity_To_ModelWithProviderIdDto() - { - // Arrange - var entity = new Model - { - Id = 99, - Name = "claude-3-opus", - ModelSeriesId = 2, - ModelCapabilitiesId = 8, - Capabilities = new ConduitLLM.Configuration.Entities.ModelCapabilities - { - Id = 8, - SupportsChat = true, - MaxTokens = 200000 - }, - Identifiers = new List - { - new ModelIdentifier - { - Id = 1, - ModelId = 99, - Provider = "anthropic", - Identifier = "claude-3-opus-20240229", - IsPrimary = true - }, - new ModelIdentifier - { - Id = 2, - ModelId = 99, - Provider = "azure", - Identifier = "my-claude-deployment", - IsPrimary = false - } - }, - IsActive = true, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }; - - var provider = "anthropic"; - - // Act - simulate controller logic for provider-specific DTO - var providerIdentifier = entity.Identifiers?.FirstOrDefault(i => - string.Equals(i.Provider, provider, StringComparison.OrdinalIgnoreCase))?.Identifier - ?? entity.Name; - - var dto = new ModelWithProviderIdDto - { - Id = entity.Id, - Name = entity.Name, - ModelSeriesId = entity.ModelSeriesId, - ModelCapabilitiesId = entity.ModelCapabilitiesId, - ProviderModelId = providerIdentifier, - Capabilities = entity.Capabilities != null ? MapCapabilitiesToDto(entity.Capabilities) : null, - IsActive = entity.IsActive, - CreatedAt = entity.CreatedAt, - UpdatedAt = entity.UpdatedAt - }; - - // Assert - dto.Should().NotBeNull(); - dto.ProviderModelId.Should().Be("claude-3-opus-20240229"); - dto.Name.Should().Be("claude-3-opus"); - } - - [Fact] - public void Should_Use_Model_Name_As_Fallback_For_ProviderModelId() - { - // Arrange - var entity = new Model - { - Id = 1, - Name = "generic-model", - ModelSeriesId = 1, - ModelCapabilitiesId = 1, - Identifiers = new List(), // No identifiers - IsActive = true, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }; - - var provider = "unknown-provider"; - - // Act - var providerIdentifier = entity.Identifiers?.FirstOrDefault(i => - string.Equals(i.Provider, provider, StringComparison.OrdinalIgnoreCase))?.Identifier - ?? entity.Name; // Fallback to name - - // Assert - providerIdentifier.Should().Be("generic-model"); - } - - // Helper methods that mirror controller mapping logic - private static ModelDto MapEntityToDto(Model entity) - { - return new ModelDto - { - Id = entity.Id, - Name = entity.Name, - ModelSeriesId = entity.ModelSeriesId, - ModelCapabilitiesId = entity.ModelCapabilitiesId, - Capabilities = entity.Capabilities != null ? MapCapabilitiesToDto(entity.Capabilities) : null, - IsActive = entity.IsActive, - CreatedAt = entity.CreatedAt, - UpdatedAt = entity.UpdatedAt - }; - } - - private static ModelCapabilitiesDto MapCapabilitiesToDto(ConduitLLM.Configuration.Entities.ModelCapabilities capabilities) - { - return new ModelCapabilitiesDto - { - Id = capabilities.Id, - SupportsChat = capabilities.SupportsChat, - 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 - }; - } - - } -} \ No newline at end of file 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/Admin/Services/AdminModelCostServiceTests.Create.cs b/ConduitLLM.Tests/Admin/Services/AdminModelCostServiceTests.Create.cs deleted file mode 100644 index 136676690..000000000 --- a/ConduitLLM.Tests/Admin/Services/AdminModelCostServiceTests.Create.cs +++ /dev/null @@ -1,156 +0,0 @@ -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.Entities; - -using FluentAssertions; - -using Moq; - -namespace ConduitLLM.Tests.Admin.Services -{ - public partial class AdminModelCostServiceTests - { - #region CreateModelCostAsync Tests - - [Fact] - public async Task CreateModelCostAsync_WithValidData_ShouldCreateModelCost() - { - // Arrange - var createDto = new CreateModelCostDto - { - CostName = "Test Model Cost", - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m, - ModelProviderMappingIds = new List() - }; - - var createdEntity = new ModelCost - { - Id = 1, - CostName = createDto.CostName, - InputCostPerMillionTokens = createDto.InputCostPerMillionTokens, - OutputCostPerMillionTokens = createDto.OutputCostPerMillionTokens, - ModelCostMappings = new List() - }; - - _mockModelCostRepository.Setup(x => x.GetByCostNameAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync((ModelCost?)null); - _mockModelCostRepository.Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(1); - _mockModelCostRepository.Setup(x => x.GetByIdAsync(1, It.IsAny())) - .ReturnsAsync(createdEntity); - - // Act - var result = await _service.CreateModelCostAsync(createDto); - - // Assert - result.Should().NotBeNull(); - result.CostName.Should().Be("Test Model Cost"); - result.InputCostPerMillionTokens.Should().Be(10.00m); - result.OutputCostPerMillionTokens.Should().Be(20.00m); - } - - [Fact] - public async Task CreateModelCostAsync_WithModelProviderMappingIds_ShouldCreateMappings() - { - // Arrange - var mappingIds = new List { 1, 2, 3 }; - var createDto = new CreateModelCostDto - { - CostName = "Test Model Cost with Mappings", - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m, - ModelProviderMappingIds = mappingIds - }; - - var createdEntity = new ModelCost - { - Id = 1, - CostName = createDto.CostName, - InputCostPerMillionTokens = createDto.InputCostPerMillionTokens, - OutputCostPerMillionTokens = createDto.OutputCostPerMillionTokens, - ModelCostMappings = new List - { - new ModelCostMapping - { - ModelCostId = 1, - ModelProviderMappingId = 1, - IsActive = true, - ModelProviderMapping = new ModelProviderMapping { Id = 1, ModelAlias = "model1", ModelId = 1 } - }, - new ModelCostMapping - { - ModelCostId = 1, - ModelProviderMappingId = 2, - IsActive = true, - ModelProviderMapping = new ModelProviderMapping { Id = 2, ModelAlias = "model2", ModelId = 1 } - }, - new ModelCostMapping - { - ModelCostId = 1, - ModelProviderMappingId = 3, - IsActive = true, - ModelProviderMapping = new ModelProviderMapping { Id = 3, ModelAlias = "model3", ModelId = 1 } - } - } - }; - - _mockModelCostRepository.Setup(x => x.GetByCostNameAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync((ModelCost?)null); - _mockModelCostRepository.Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(1); - _mockModelCostRepository.Setup(x => x.GetByIdAsync(1, It.IsAny())) - .ReturnsAsync(createdEntity); - - // Act - var result = await _service.CreateModelCostAsync(createDto); - - // Assert - result.Should().NotBeNull(); - result.AssociatedModelAliases.Should().HaveCount(3); - result.AssociatedModelAliases.Should().Contain(new[] { "model1", "model2", "model3" }); - - // Verify mappings were added to DbContext - using (var dbContext = CreateDbContext()) - { - var mappings = dbContext.ModelCostMappings.ToList(); - mappings.Should().HaveCount(3); - mappings.Should().AllSatisfy(m => - { - m.ModelCostId.Should().Be(1); - m.IsActive.Should().BeTrue(); - }); - } - } - - [Fact] - public async Task CreateModelCostAsync_WithDuplicateName_ShouldThrowInvalidOperationException() - { - // Arrange - var createDto = new CreateModelCostDto - { - CostName = "Existing Cost", - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m - }; - - var existingCost = new ModelCost { Id = 1, CostName = "Existing Cost" }; - - _mockModelCostRepository.Setup(x => x.GetByCostNameAsync("Existing Cost", It.IsAny())) - .ReturnsAsync(existingCost); - - // Act & Assert - await Assert.ThrowsAsync( - async () => await _service.CreateModelCostAsync(createDto)); - } - - [Fact] - public async Task CreateModelCostAsync_WithNullDto_ShouldThrowArgumentNullException() - { - // Act & Assert - await Assert.ThrowsAsync( - async () => await _service.CreateModelCostAsync(null!)); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Services/AdminModelCostServiceTests.EdgeCases.cs b/ConduitLLM.Tests/Admin/Services/AdminModelCostServiceTests.EdgeCases.cs deleted file mode 100644 index 1f3c58330..000000000 --- a/ConduitLLM.Tests/Admin/Services/AdminModelCostServiceTests.EdgeCases.cs +++ /dev/null @@ -1,136 +0,0 @@ -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.Entities; - -using FluentAssertions; - -using Moq; - -namespace ConduitLLM.Tests.Admin.Services -{ - public partial class AdminModelCostServiceTests - { - #region Edge Cases and Bug Prevention Tests - - [Fact] - public async Task CreateModelCostAsync_WithEmptyMappingIds_ShouldNotCreateMappings() - { - // This test ensures we handle empty mapping lists correctly - var createDto = new CreateModelCostDto - { - CostName = "Cost without mappings", - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m, - ModelProviderMappingIds = new List() // Empty list - }; - - var createdEntity = new ModelCost - { - Id = 1, - CostName = createDto.CostName, - ModelCostMappings = new List() - }; - - _mockModelCostRepository.Setup(x => x.GetByCostNameAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync((ModelCost?)null); - _mockModelCostRepository.Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(1); - _mockModelCostRepository.Setup(x => x.GetByIdAsync(1, It.IsAny())) - .ReturnsAsync(createdEntity); - - // Act - var result = await _service.CreateModelCostAsync(createDto); - - // Assert - result.Should().NotBeNull(); - result.AssociatedModelAliases.Should().BeEmpty(); - using (var verifyContext = CreateDbContext()) - { - verifyContext.ModelCostMappings.Should().BeEmpty(); - } - } - - [Fact] - public async Task UpdateModelCostAsync_RemoveAllMappings_ShouldClearMappings() - { - // This test ensures we can remove all mappings by passing an empty list - var updateDto = new UpdateModelCostDto - { - Id = 1, - CostName = "Updated Cost", - InputCostPerMillionTokens = 15.00m, - OutputCostPerMillionTokens = 25.00m, - ModelProviderMappingIds = new List() // Empty list to clear all mappings - }; - - var existingCost = new ModelCost - { - Id = 1, - CostName = "Original Cost" - }; - - // Add existing mappings - using (var setupContext = CreateDbContext()) - { - setupContext.ModelCostMappings.AddRange(new[] - { - new ModelCostMapping { Id = 1, ModelCostId = 1, ModelProviderMappingId = 1 }, - new ModelCostMapping { Id = 2, ModelCostId = 1, ModelProviderMappingId = 2 } - }); - await setupContext.SaveChangesAsync(); - } - - _mockModelCostRepository.Setup(x => x.GetByIdAsync(1, It.IsAny())) - .ReturnsAsync(existingCost); - _mockModelCostRepository.Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(true); - - // Act - var result = await _service.UpdateModelCostAsync(updateDto); - - // Assert - result.Should().BeTrue(); - using (var verifyContext = CreateDbContext()) - { - verifyContext.ModelCostMappings.Where(m => m.ModelCostId == 1).Should().BeEmpty(); - } - } - - [Fact] - public async Task ToDto_WithInactiveMappings_ShouldOnlyReturnActiveModelAliases() - { - // This test verifies the bug fix where only active mappings should be included - var modelCost = new ModelCost - { - Id = 1, - CostName = "Test Cost", - ModelCostMappings = new List - { - new ModelCostMapping - { - IsActive = true, - ModelProviderMapping = new ModelProviderMapping { ModelAlias = "active-model", ModelId = 1 } - }, - new ModelCostMapping - { - IsActive = false, // Inactive mapping - ModelProviderMapping = new ModelProviderMapping { ModelAlias = "inactive-model", ModelId = 1 } - } - } - }; - - _mockModelCostRepository.Setup(x => x.GetByIdAsync(1, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.GetModelCostByIdAsync(1); - - // Assert - result.Should().NotBeNull(); - result!.AssociatedModelAliases.Should().HaveCount(1); - result.AssociatedModelAliases.Should().Contain("active-model"); - result.AssociatedModelAliases.Should().NotContain("inactive-model"); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Services/AdminModelCostServiceTests.Update.cs b/ConduitLLM.Tests/Admin/Services/AdminModelCostServiceTests.Update.cs deleted file mode 100644 index 74675ac3c..000000000 --- a/ConduitLLM.Tests/Admin/Services/AdminModelCostServiceTests.Update.cs +++ /dev/null @@ -1,164 +0,0 @@ -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.Entities; - -using FluentAssertions; - -using Moq; - -namespace ConduitLLM.Tests.Admin.Services -{ - public partial class AdminModelCostServiceTests - { - #region UpdateModelCostAsync Tests - - [Fact] - public async Task UpdateModelCostAsync_WithValidData_ShouldUpdateModelCost() - { - // Arrange - var updateDto = new UpdateModelCostDto - { - Id = 1, - CostName = "Updated Cost Name", - InputCostPerMillionTokens = 15.00m, - OutputCostPerMillionTokens = 25.00m, - ModelProviderMappingIds = new List() - }; - - var existingCost = new ModelCost - { - Id = 1, - CostName = "Original Cost Name", - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m - }; - - _mockModelCostRepository.Setup(x => x.GetByIdAsync(1, It.IsAny())) - .ReturnsAsync(existingCost); - _mockModelCostRepository.Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(true); - - // Act - var result = await _service.UpdateModelCostAsync(updateDto); - - // Assert - result.Should().BeTrue(); - _mockModelCostRepository.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task UpdateModelCostAsync_WithModelProviderMappingIds_ShouldUpdateMappings() - { - // Arrange - var newMappingIds = new List { 4, 5 }; - var updateDto = new UpdateModelCostDto - { - Id = 1, - CostName = "Updated Cost", - InputCostPerMillionTokens = 15.00m, - OutputCostPerMillionTokens = 25.00m, - ModelProviderMappingIds = newMappingIds - }; - - var existingCost = new ModelCost - { - Id = 1, - CostName = "Original Cost", - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m - }; - - // Add existing mappings to test context - using (var setupContext = CreateDbContext()) - { - setupContext.ModelCostMappings.AddRange(new[] - { - new ModelCostMapping { Id = 1, ModelCostId = 1, ModelProviderMappingId = 1, IsActive = true }, - new ModelCostMapping { Id = 2, ModelCostId = 1, ModelProviderMappingId = 2, IsActive = true }, - new ModelCostMapping { Id = 3, ModelCostId = 1, ModelProviderMappingId = 3, IsActive = true } - }); - await setupContext.SaveChangesAsync(); - } - - _mockModelCostRepository.Setup(x => x.GetByIdAsync(1, It.IsAny())) - .ReturnsAsync(existingCost); - _mockModelCostRepository.Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(true); - - // Act - var result = await _service.UpdateModelCostAsync(updateDto); - - // Assert - result.Should().BeTrue(); - - // Verify old mappings were removed and new ones added - using (var verifyContext = CreateDbContext()) - { - var mappings = verifyContext.ModelCostMappings.Where(m => m.ModelCostId == 1).ToList(); - mappings.Should().HaveCount(2); - mappings.Select(m => m.ModelProviderMappingId).Should().BeEquivalentTo(new[] { 4, 5 }); - mappings.Should().AllSatisfy(m => m.IsActive.Should().BeTrue()); - } - } - - [Fact] - public async Task UpdateModelCostAsync_WithNonExistentId_ShouldReturnFalse() - { - // Arrange - var updateDto = new UpdateModelCostDto - { - Id = 999, - CostName = "Non-existent", - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m - }; - - _mockModelCostRepository.Setup(x => x.GetByIdAsync(999, It.IsAny())) - .ReturnsAsync((ModelCost?)null); - - // Act - var result = await _service.UpdateModelCostAsync(updateDto); - - // Assert - result.Should().BeFalse(); - _mockModelCostRepository.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); - } - - [Fact] - public async Task UpdateModelCostAsync_WithDuplicateName_ShouldThrowInvalidOperationException() - { - // Arrange - var updateDto = new UpdateModelCostDto - { - Id = 1, - CostName = "Existing Other Cost", - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m - }; - - var existingCost = new ModelCost - { - Id = 1, - CostName = "Original Cost", - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m - }; - - var otherCost = new ModelCost - { - Id = 2, - CostName = "Existing Other Cost" - }; - - _mockModelCostRepository.Setup(x => x.GetByIdAsync(1, It.IsAny())) - .ReturnsAsync(existingCost); - _mockModelCostRepository.Setup(x => x.GetByCostNameAsync("Existing Other Cost", It.IsAny())) - .ReturnsAsync(otherCost); - - // Act & Assert - await Assert.ThrowsAsync( - async () => await _service.UpdateModelCostAsync(updateDto)); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Architecture/ModelArchitectureTests.cs b/ConduitLLM.Tests/Architecture/ModelArchitectureTests.cs deleted file mode 100644 index 3ee29d3be..000000000 --- a/ConduitLLM.Tests/Architecture/ModelArchitectureTests.cs +++ /dev/null @@ -1,246 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Architecture -{ - /// - /// Architecture tests to ensure the Model capability pattern is correctly implemented - /// and to prevent regressions in the architectural design - /// - public class ModelArchitectureTests - { - [Fact] - public void ModelProviderMapping_CapabilityProperties_ShouldBeReadOnly() - { - // Arrange - var mappingType = typeof(ModelProviderMapping); - var capabilityProperties = mappingType.GetProperties() - .Where(p => p.Name.StartsWith("Supports") || - p.Name == "MaxContextTokens" || - p.Name == "TokenizerType") - .ToList(); - - // Act & Assert - foreach (var property in capabilityProperties) - { - // Skip properties that have legitimate setters - if (property.Name == "MaxContextTokensOverride") - continue; - - Assert.False(property.CanWrite, - $"Property {property.Name} should be read-only as it derives from Model.Capabilities"); - } - } - - [Fact] - public void ModelProviderMapping_ShouldHaveModelIdRequired() - { - // Arrange - var mappingType = typeof(ModelProviderMapping); - var modelIdProperty = mappingType.GetProperty("ModelId"); - - // Assert - Assert.NotNull(modelIdProperty); - Assert.Equal(typeof(int), modelIdProperty.PropertyType); - // ModelId should not be nullable - Assert.False(Nullable.GetUnderlyingType(modelIdProperty.PropertyType) != null, - "ModelId should not be nullable"); - } - - [Fact] - public void Model_ShouldHaveCapabilitiesRelationship() - { - // Arrange - var modelType = typeof(Model); - - // Act - var capabilitiesProperty = modelType.GetProperty("Capabilities"); - var capabilitiesIdProperty = modelType.GetProperty("ModelCapabilitiesId"); - - // Assert - Assert.NotNull(capabilitiesProperty); - Assert.Equal(typeof(ModelCapabilities), capabilitiesProperty.PropertyType); - - Assert.NotNull(capabilitiesIdProperty); - Assert.Equal(typeof(int), capabilitiesIdProperty.PropertyType); - } - - [Fact] - public void ModelCapabilities_ShouldHaveAllExpectedProperties() - { - // Arrange - var capabilitiesType = typeof(ModelCapabilities); - var expectedProperties = new[] - { - "SupportsChat", - "SupportsVision", - "SupportsAudioTranscription", - "SupportsTextToSpeech", - "SupportsRealtimeAudio", - "SupportsImageGeneration", - "SupportsVideoGeneration", - "SupportsEmbeddings", - "SupportsFunctionCalling", - "SupportsStreaming", - "MaxTokens", - "TokenizerType" - }; - - // Act & Assert - foreach (var propertyName in expectedProperties) - { - var property = capabilitiesType.GetProperty(propertyName); - Assert.NotNull(property); - Assert.True(property.CanRead && property.CanWrite, - $"Property {propertyName} should be read-write in ModelCapabilities"); - } - } - - [Fact] - public void ModelProviderMapping_MaxContextTokens_ShouldRespectOverride() - { - // Arrange - var mapping = new ModelProviderMapping - { - ModelId = 1, - Model = new Model - { - Id = 1, - Name = "test-model", - Capabilities = new ModelCapabilities - { - MaxTokens = 4096 - } - } - }; - - // Act & Assert - Without override - Assert.Equal(4096, mapping.MaxContextTokens); - - // Act & Assert - With override - mapping.MaxContextTokensOverride = 8192; - Assert.Equal(8192, mapping.MaxContextTokens); - - // Act & Assert - Remove override - mapping.MaxContextTokensOverride = null; - Assert.Equal(4096, mapping.MaxContextTokens); - } - - [Fact] - public void Model_ShouldHaveRequiredRelationships() - { - // Arrange - var modelType = typeof(Model); - - // Act - var seriesProperty = modelType.GetProperty("Series"); - var seriesIdProperty = modelType.GetProperty("ModelSeriesId"); - var identifiersProperty = modelType.GetProperty("Identifiers"); - var mappingsProperty = modelType.GetProperty("ProviderMappings"); - - // Assert - Model Series relationship - Assert.NotNull(seriesProperty); - Assert.Equal(typeof(ModelSeries), seriesProperty.PropertyType); - Assert.NotNull(seriesIdProperty); - - // Assert - Collections - Assert.NotNull(identifiersProperty); - Assert.True(identifiersProperty.PropertyType.IsGenericType); - - Assert.NotNull(mappingsProperty); - Assert.True(mappingsProperty.PropertyType.IsGenericType); - } - - [Fact] - public void ModelProviderMapping_ShouldDeriveAllCapabilitiesFromModel() - { - // Arrange - var model = new Model - { - Id = 1, - Name = "test-model", - Capabilities = new ModelCapabilities - { - SupportsChat = true, - SupportsVision = true, - SupportsEmbeddings = false, - SupportsFunctionCalling = true, - SupportsStreaming = true, - SupportsAudioTranscription = true, - SupportsTextToSpeech = false, - SupportsRealtimeAudio = false, - SupportsImageGeneration = false, - SupportsVideoGeneration = false, - MaxTokens = 8192, - TokenizerType = TokenizerType.Cl100KBase - } - }; - - var mapping = new ModelProviderMapping - { - ModelId = 1, - Model = model - }; - - // Act & Assert - Assert.Equal(model.Capabilities.SupportsChat, mapping.SupportsChat); - Assert.Equal(model.Capabilities.SupportsVision, mapping.SupportsVision); - 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); - Assert.Equal(model.Capabilities.TokenizerType, mapping.TokenizerType); - } - - [Fact] - public void ModelProviderMapping_WithNullModel_ShouldReturnDefaultCapabilities() - { - // Arrange - var mapping = new ModelProviderMapping - { - ModelId = 1, - Model = null // Simulating lazy loading not yet loaded - }; - - // Act & Assert - Should return false/default values when Model is null - Assert.False(mapping.SupportsChat); - Assert.False(mapping.SupportsVision); - Assert.False(mapping.SupportsEmbeddings); - // MaxContextTokens has a default of 4096 when Model is null - Assert.Equal(4096, mapping.MaxContextTokens); - Assert.Null(mapping.TokenizerType); - } - - [Fact] - public void Model_IsActive_ShouldDefaultToTrue() - { - // Arrange & Act - var model = new Model(); - - // Assert - Assert.True(model.IsActive); - } - - [Fact] - public void Model_TimestampProperties_ShouldExist() - { - // Arrange - var modelType = typeof(Model); - - // Act - var createdAtProperty = modelType.GetProperty("CreatedAt"); - var updatedAtProperty = modelType.GetProperty("UpdatedAt"); - - // Assert - Assert.NotNull(createdAtProperty); - Assert.Equal(typeof(DateTime), createdAtProperty.PropertyType); - - Assert.NotNull(updatedAtProperty); - Assert.Equal(typeof(DateTime), updatedAtProperty.PropertyType); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/ConduitLLM.Tests.csproj b/ConduitLLM.Tests/ConduitLLM.Tests.csproj deleted file mode 100644 index 7cdc169a9..000000000 --- a/ConduitLLM.Tests/ConduitLLM.Tests.csproj +++ /dev/null @@ -1,81 +0,0 @@ - - - - net9.0 - enable - disable - false - xUnit2013;CS8632 - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - - 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/Configuration/Repositories/ModelProviderMappingRepositoryTests.cs b/ConduitLLM.Tests/Configuration/Repositories/ModelProviderMappingRepositoryTests.cs deleted file mode 100644 index a70174fca..000000000 --- a/ConduitLLM.Tests/Configuration/Repositories/ModelProviderMappingRepositoryTests.cs +++ /dev/null @@ -1,239 +0,0 @@ -using Microsoft.Extensions.Logging; -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.Repositories; -using ConduitLLM.Tests.TestInfrastructure; -using ConduitLLM.Tests.Helpers; - -namespace ConduitLLM.Tests.Configuration.Repositories -{ - /// - /// Repository tests that work correctly with SQLite - /// - [Collection("RepositoryTests")] - public class ModelProviderMappingRepositoryTests : RepositoryTestBase - { - private readonly ILogger _logger; - - public ModelProviderMappingRepositoryTests() - { - _logger = new LoggerFactory().CreateLogger(); - } - - [Fact] - public async Task GetByIdAsync_ShouldReturnMappingWithModelCapabilities() - { - // Arrange - int mappingId = 0; - - SeedData(context => - { - // Add provider first - var provider = new Provider - { - ProviderName = "Test Provider", - ProviderType = ProviderType.OpenAI, - BaseUrl = "https://api.openai.com/v1", - IsEnabled = true - }; - context.Providers.Add(provider); - context.SaveChanges(); - - // Create a complete model with author, series, and capabilities - var model = ModelTestHelper.CreateCompleteTestModel( - modelName: $"model-no-chat-{Guid.NewGuid()}", - supportsChat: false, - maxTokens: 4096); - - // Add the author, series, capabilities and model to the context - context.ModelAuthors.Add(model.Series.Author); - context.ModelSeries.Add(model.Series); - context.ModelCapabilities.Add(model.Capabilities); - context.Models.Add(model); - context.SaveChanges(); - - // Add mapping with FK references - var mapping = new ModelProviderMapping - { - ModelAlias = "test-mapping", - ModelId = model.Id, - ProviderModelId = "gpt-3.5", - ProviderId = provider.Id, - IsEnabled = true - }; - context.ModelProviderMappings.Add(mapping); - context.SaveChanges(); - - mappingId = mapping.Id; - }); - - var repository = new ModelProviderMappingRepository(CreateDbContextFactory(), _logger); - - // Act - var mapping = await repository.GetByIdAsync(mappingId); - - // Assert - Assert.NotNull(mapping); - Assert.NotNull(mapping.Model); - Assert.NotNull(mapping.Model.Capabilities); - Assert.False(mapping.SupportsChat); - Assert.Equal(4096, mapping.MaxContextTokens); - } - - [Fact(Skip = "SQLite constraint issue - test creates duplicate data within single test method")] - public async Task UpdateAsync_ChangingModelId_ShouldUpdateCapabilities() - { - // Arrange - int mappingId = 0; - int modelWithChatId = 0; - var testId = Guid.NewGuid(); - - SeedData(context => - { - // Add provider - var provider = new Provider - { - ProviderName = "Test Provider", - ProviderType = ProviderType.OpenAI, - BaseUrl = "https://api.openai.com/v1", - IsEnabled = true - }; - context.Providers.Add(provider); - context.SaveChanges(); - - // Add model series - var series = new ModelSeries - { - Name = $"Test Series {testId}" - }; - context.ModelSeries.Add(series); - context.SaveChanges(); - - // Add capabilities for both models - var capabilitiesNoChat = new ModelCapabilities - { - SupportsChat = false, - MaxTokens = 4096 - }; - context.ModelCapabilities.Add(capabilitiesNoChat); - - var capabilitiesWithChat = new ModelCapabilities - { - SupportsChat = true, - SupportsVision = true, - SupportsStreaming = true, - MaxTokens = 8192 - }; - context.ModelCapabilities.Add(capabilitiesWithChat); - context.SaveChanges(); - - // Add models with FK references - var modelNoChat = new Model - { - Name = $"model-no-chat-{testId}", - ModelSeriesId = series.Id, - ModelCapabilitiesId = capabilitiesNoChat.Id - }; - context.Models.Add(modelNoChat); - - var modelWithChat = new Model - { - Name = $"model-with-chat-{testId}", - ModelSeriesId = series.Id, - ModelCapabilitiesId = capabilitiesWithChat.Id - }; - context.Models.Add(modelWithChat); - context.SaveChanges(); - - // Add mapping with FK references - var mapping = new ModelProviderMapping - { - ModelAlias = "test-mapping", - ModelId = modelNoChat.Id, - ProviderModelId = "gpt-3.5", - ProviderId = provider.Id, - IsEnabled = true - }; - context.ModelProviderMappings.Add(mapping); - context.SaveChanges(); - - mappingId = mapping.Id; - modelWithChatId = modelWithChat.Id; - }); - - var repository = new ModelProviderMappingRepository(CreateDbContextFactory(), _logger); - - // Get initial mapping - var mapping = await repository.GetByIdAsync(mappingId); - Assert.NotNull(mapping); - Assert.False(mapping.SupportsChat); // Initially false - - // Act - Change to model with chat support - mapping.ModelId = modelWithChatId; - await repository.UpdateAsync(mapping); - - // Assert - ModelId should be updated - var updated = await repository.GetByIdAsync(mappingId); - Assert.NotNull(updated); - Assert.Equal(modelWithChatId, updated.ModelId); - } - - [Fact] - public async Task MaxContextTokensOverride_ShouldTakePrecedence() - { - // Arrange - int mappingId = 0; - - SeedData(context => - { - // Add provider - var provider = new Provider - { - ProviderName = "Test Provider", - ProviderType = ProviderType.OpenAI, - BaseUrl = "https://api.openai.com/v1", - IsEnabled = true - }; - context.Providers.Add(provider); - context.SaveChanges(); - - // Create a complete model with author, series, and capabilities - var model = ModelTestHelper.CreateCompleteTestModel( - modelName: $"model-override-test-{Guid.NewGuid()}", - supportsChat: false, - maxTokens: 4096); - - // Add the author, series, capabilities and model to the context - context.ModelAuthors.Add(model.Series.Author); - context.ModelSeries.Add(model.Series); - context.ModelCapabilities.Add(model.Capabilities); - context.Models.Add(model); - context.SaveChanges(); - - // Add mapping with override - var mapping = new ModelProviderMapping - { - ModelAlias = "override-test", - ModelId = model.Id, - ProviderModelId = "test", - ProviderId = provider.Id, - MaxContextTokensOverride = 16384, - IsEnabled = true - }; - context.ModelProviderMappings.Add(mapping); - context.SaveChanges(); - - mappingId = mapping.Id; - }); - - var repository = new ModelProviderMappingRepository(CreateDbContextFactory(), _logger); - - // Act - var mapping = await repository.GetByIdAsync(mappingId); - - // Assert - Assert.NotNull(mapping); - Assert.Equal(16384, mapping.MaxContextTokens); // Override value, not model's 4096 - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Configuration/SimpleSupportsChatTests.cs b/ConduitLLM.Tests/Configuration/SimpleSupportsChatTests.cs deleted file mode 100644 index 5b46b2137..000000000 --- a/ConduitLLM.Tests/Configuration/SimpleSupportsChatTests.cs +++ /dev/null @@ -1,166 +0,0 @@ -// TODO: Update tests for new Model architecture where capabilities come from Model entity -using FluentAssertions; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Configuration -{ - /// - /// Simple tests for the SupportsChat property - /// - public class SimpleSupportsChatTests - { - [Fact] - public void ModelProviderMapping_SupportsChatProperty_ComesFromModel() - { - // Arrange - var model = new Model - { - Id = 1, - Name = "test-model", - Capabilities = new ModelCapabilities - { - SupportsChat = true - } - }; - - var mapping = new ModelProviderMapping - { - ModelId = 1, - Model = model - }; - - // Assert - mapping.SupportsChat.Should().BeTrue(); - } - - [Fact] - public void ModelProviderMapping_SupportsChatDefaultValue_IsFalseWhenNoModel() - { - // Arrange & Act - var mapping = new ModelProviderMapping(); - - // Assert - returns false when Model is null - mapping.SupportsChat.Should().BeFalse(); - } - - [Fact] - public void ModelProviderMapping_AllCapabilityFlags_DefaultToFalseWhenNoModel() - { - // Arrange & Act - var mapping = new ModelProviderMapping(); - - // Assert - all return false when Model is null - mapping.SupportsChat.Should().BeFalse(); - mapping.SupportsEmbeddings.Should().BeFalse(); - mapping.SupportsVision.Should().BeFalse(); - mapping.SupportsImageGeneration.Should().BeFalse(); - mapping.SupportsFunctionCalling.Should().BeFalse(); - } - - [Fact] - public void ModelProviderMapping_CapabilitiesReflectModelCapabilities() - { - // Arrange - var model = new Model - { - Id = 1, - Name = "test-model", - Capabilities = new ModelCapabilities - { - SupportsChat = true, - SupportsVision = true, - SupportsFunctionCalling = true, - SupportsStreaming = false, - SupportsEmbeddings = false - } - }; - - var mapping = new ModelProviderMapping - { - ModelId = 1, - Model = model - }; - - // Assert - capabilities come from the Model - mapping.SupportsChat.Should().BeTrue(); - mapping.SupportsVision.Should().BeTrue(); - mapping.SupportsFunctionCalling.Should().BeTrue(); - mapping.SupportsStreaming.Should().BeFalse(); - mapping.SupportsEmbeddings.Should().BeFalse(); - } - - [Fact] - public void ModelProviderMapping_MaxContextTokens_UsesOverrideWhenSet() - { - // Arrange - var model = new Model - { - Id = 1, - Name = "test-model", - Capabilities = new ModelCapabilities - { - MaxTokens = 4096 - } - }; - - var mapping = new ModelProviderMapping - { - ModelId = 1, - Model = model, - MaxContextTokensOverride = 8192 - }; - - // Assert - override takes precedence - mapping.MaxContextTokens.Should().Be(8192); - } - - [Fact] - public void ModelProviderMapping_MaxContextTokens_UsesModelCapabilitiesWhenNoOverride() - { - // Arrange - var model = new Model - { - Id = 1, - Name = "test-model", - Capabilities = new ModelCapabilities - { - MaxTokens = 4096 - } - }; - - var mapping = new ModelProviderMapping - { - ModelId = 1, - Model = model, - MaxContextTokensOverride = null - }; - - // Assert - uses Model's capabilities - mapping.MaxContextTokens.Should().Be(4096); - } - - [Fact] - public void ModelProviderMapping_TokenizerType_ComesFromModelCapabilities() - { - // Arrange - var model = new Model - { - Id = 1, - Name = "test-model", - Capabilities = new ModelCapabilities - { - TokenizerType = TokenizerType.Cl100KBase - } - }; - - var mapping = new ModelProviderMapping - { - ModelId = 1, - Model = model - }; - - // Assert - mapping.TokenizerType.Should().Be(TokenizerType.Cl100KBase); - } - } -} \ No newline at end of file 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/CacheManagerTests.cs b/ConduitLLM.Tests/Core/Services/CacheManagerTests.cs deleted file mode 100644 index ab7c16a5c..000000000 --- a/ConduitLLM.Tests/Core/Services/CacheManagerTests.cs +++ /dev/null @@ -1,355 +0,0 @@ -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Services; - -namespace ConduitLLM.Tests.Core.Services -{ - public class CacheManagerTests : IDisposable - { - private readonly Mock> _loggerMock; - private readonly IMemoryCache _memoryCache; - private readonly Mock _distributedCacheMock; - - public CacheManagerTests() - { - _loggerMock = new Mock>(); - _memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - _distributedCacheMock = new Mock(); - } - - [Fact] - public async Task GetAsync_WhenValueInMemoryCache_ReturnsValueWithoutDistributedLookup() - { - // Arrange - var cacheManager = new CacheManager(_memoryCache, _distributedCacheMock.Object, _loggerMock.Object); - const string key = "test-key"; - const string value = "test-value"; - const CacheRegion region = CacheRegion.Default; - - await cacheManager.SetAsync(key, value, region); - - // Act - var result = await cacheManager.GetAsync(key, region); - - // Assert - Assert.Equal(value, result); - _distributedCacheMock.Verify(x => x.GetAsync(It.IsAny(), default), Times.Never); - } - - [Fact] - public async Task GetAsync_WhenValueNotInMemoryButInDistributed_ReturnsValueAndPopulatesMemory() - { - // Arrange - var distributedValue = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes("distributed-value"); - - // Mock needs to match the exact key format: "region:key" - var expectedKey = $"{CacheRegion.ProviderHealth}:test-key"; - _distributedCacheMock.Setup(x => x.GetAsync(expectedKey, default)) - .ReturnsAsync(distributedValue); - - // Also setup the broader mock to catch any key variations - _distributedCacheMock.Setup(x => x.GetAsync(It.IsAny(), default)) - .ReturnsAsync(distributedValue); - - var cacheManager = new CacheManager(_memoryCache, _distributedCacheMock.Object, _loggerMock.Object); - const string key = "test-key"; - const CacheRegion region = CacheRegion.ProviderHealth; // This region supports distributed cache - - // Debug: Check if distributed cache is enabled - var config = cacheManager.GetRegionConfig(region); - var debugMsg = $"UseDistributedCache: {config.UseDistributedCache}, DistCache!=null: {_distributedCacheMock.Object != null}"; - - // Act - var result = await cacheManager.GetAsync(key, region); - - // Assert - Assert.True(result == "distributed-value", $"Expected 'distributed-value', got '{result}'. Debug: {debugMsg}"); - _distributedCacheMock.Verify(x => x.GetAsync(expectedKey, default), Times.Once); - - // Verify memory cache was populated - var memoryCachedValue = await cacheManager.GetAsync(key, region); - Assert.Equal("distributed-value", memoryCachedValue); - _distributedCacheMock.Verify(x => x.GetAsync(expectedKey, default), Times.Once); // Still only once - } - - [Fact] - public async Task SetAsync_SetsInBothMemoryAndDistributedCache() - { - // Arrange - var cacheManager = new CacheManager(_memoryCache, _distributedCacheMock.Object, _loggerMock.Object); - const string key = "test-key"; - const string value = "test-value"; - const CacheRegion region = CacheRegion.VirtualKeys; - - // Act - await cacheManager.SetAsync(key, value, region); - - // Assert - var memoryResult = _memoryCache.Get($"{region}:{key}"); - Assert.Equal(value, memoryResult); - - _distributedCacheMock.Verify(x => x.SetAsync( - It.Is(k => k == $"{region}:{key}"), - It.IsAny(), - It.IsAny(), - default), Times.Once); - } - - [Fact] - public async Task GetOrCreateAsync_WhenCacheMiss_CallsFactoryAndCachesResult() - { - // Arrange - var cacheManager = new CacheManager(_memoryCache, _distributedCacheMock.Object, _loggerMock.Object); - const string key = "test-key"; - const string expectedValue = "factory-value"; - const CacheRegion region = CacheRegion.ModelMetadata; - var factoryCalled = false; - - // Act - var result = await cacheManager.GetOrCreateAsync( - key, - async () => - { - factoryCalled = true; - await Task.Delay(10); // Simulate async work - return expectedValue; - }, - region); - - // Assert - Assert.True(factoryCalled); - Assert.Equal(expectedValue, result); - - // Verify value was cached - var cachedValue = await cacheManager.GetAsync(key, region); - Assert.Equal(expectedValue, cachedValue); - } - - [Fact] - public async Task GetOrCreateAsync_WhenCacheHit_DoesNotCallFactory() - { - // Arrange - var cacheManager = new CacheManager(_memoryCache, _distributedCacheMock.Object, _loggerMock.Object); - const string key = "test-key"; - const string cachedValue = "cached-value"; - const CacheRegion region = CacheRegion.ModelMetadata; - - // Pre-populate cache - await cacheManager.SetAsync(key, cachedValue, region); - - var factoryCalled = false; - - // Act - var result = await cacheManager.GetOrCreateAsync( - key, - async () => - { - factoryCalled = true; - await Task.Delay(10); - return "factory-value"; - }, - region); - - // Assert - Assert.False(factoryCalled); - Assert.Equal(cachedValue, result); - } - - [Fact] - public async Task RemoveAsync_RemovesFromBothCaches() - { - // Arrange - var cacheManager = new CacheManager(_memoryCache, _distributedCacheMock.Object, _loggerMock.Object); - const string key = "test-key"; - const string value = "test-value"; - const CacheRegion region = CacheRegion.RateLimits; - - // Pre-populate cache - await cacheManager.SetAsync(key, value, region); - - // Act - var removed = await cacheManager.RemoveAsync(key, region); - - // Assert - Assert.True(removed); - var result = await cacheManager.GetAsync(key, region); - Assert.Null(result); - - _distributedCacheMock.Verify(x => x.RemoveAsync( - It.Is(k => k == $"{region}:{key}"), - default), Times.Once); - } - - [Fact] - public void RegionConfig_AppliesCorrectTTL() - { - // Arrange - var options = new CacheManagerOptions - { - RegionConfigs = new Dictionary - { - [CacheRegion.AuthTokens] = new CacheRegionConfig - { - Region = CacheRegion.AuthTokens, - DefaultTTL = TimeSpan.FromMinutes(10), - MaxTTL = TimeSpan.FromMinutes(15) - } - } - }; - - var cacheManager = new CacheManager( - _memoryCache, - _distributedCacheMock.Object, - _loggerMock.Object, - Options.Create(options)); - - // Act - var config = cacheManager.GetRegionConfig(CacheRegion.AuthTokens); - - // Assert - Assert.Equal(TimeSpan.FromMinutes(10), config.DefaultTTL); - Assert.Equal(TimeSpan.FromMinutes(15), config.MaxTTL); - } - - [Fact] - public async Task Statistics_TracksHitsAndMisses() - { - // Arrange - // Setup distributed cache to return null for cache misses (to avoid exceptions) - _distributedCacheMock.Setup(x => x.GetAsync(It.IsAny(), default)) - .ReturnsAsync((byte[]?)null); - - var cacheManager = new CacheManager(_memoryCache, _distributedCacheMock.Object, _loggerMock.Object); - const CacheRegion region = CacheRegion.ProviderHealth; - - // Act - // Cache miss - var missResult = await cacheManager.GetAsync("missing-key", region); - - // Cache hit - await cacheManager.SetAsync("existing-key", "value", region); - var hitResult = await cacheManager.GetAsync("existing-key", region); - - // Get statistics - var stats = await cacheManager.GetRegionStatisticsAsync(region); - - // Assert - Assert.Equal(1, stats.HitCount); - Assert.Equal(1, stats.MissCount); - Assert.Equal(0.5, stats.HitRate); // 50% hit rate - Assert.Equal(1, stats.SetCount); - } - - [Fact] - public async Task HealthCheck_ReturnsHealthyStatus() - { - // Arrange - var cacheManager = new CacheManager(_memoryCache, _distributedCacheMock.Object, _loggerMock.Object); - - // Act - var health = await cacheManager.GetHealthStatusAsync(); - - // Assert - Assert.True(health.IsHealthy); - Assert.True(health.ComponentStatus["MemoryCache"]); - Assert.NotNull(health.MemoryCacheResponseTime); - Assert.True(health.MemoryCacheResponseTime.Value.TotalMilliseconds >= 0); - } - - [Fact] - public async Task ClearRegionAsync_ClearsOnlySpecifiedRegion() - { - // Arrange - var cacheManager = new CacheManager(_memoryCache, null, _loggerMock.Object); - - // Add items to different regions - await cacheManager.SetAsync("key1", "value1", CacheRegion.VirtualKeys); - await cacheManager.SetAsync("key2", "value2", CacheRegion.RateLimits); - - // Act - await cacheManager.ClearRegionAsync(CacheRegion.VirtualKeys); - - // Assert - var virtualKeyValue = await cacheManager.GetAsync("key1", CacheRegion.VirtualKeys); - var rateLimitValue = await cacheManager.GetAsync("key2", CacheRegion.RateLimits); - - Assert.Null(virtualKeyValue); // Should be cleared - Assert.Equal("value2", rateLimitValue); // Should remain - } - - [Fact] - public async Task ExistsAsync_ReturnsTrueForExistingKey() - { - // Arrange - var cacheManager = new CacheManager(_memoryCache, null, _loggerMock.Object); - const string key = "test-key"; - const CacheRegion region = CacheRegion.Default; - - await cacheManager.SetAsync(key, "value", region); - - // Act - var exists = await cacheManager.ExistsAsync(key, region); - var notExists = await cacheManager.ExistsAsync("non-existent", region); - - // Assert - Assert.True(exists); - Assert.False(notExists); - } - - [Fact] - public async Task RefreshAsync_UpdatesTTLWithoutChangingValue() - { - // Arrange - var cacheManager = new CacheManager(_memoryCache, _distributedCacheMock.Object, _loggerMock.Object); - const string key = "test-key"; - const string value = "test-value"; - const CacheRegion region = CacheRegion.Default; - - await cacheManager.SetAsync(key, value, region, TimeSpan.FromMinutes(5)); - - // Act - var refreshed = await cacheManager.RefreshAsync(key, region, TimeSpan.FromMinutes(30)); - - // Assert - Assert.True(refreshed); - var retrievedValue = await cacheManager.GetAsync(key, region); - Assert.Equal(value, retrievedValue); - } - - public void Dispose() - { - _memoryCache?.Dispose(); - } - - // Performance benchmark test (simplified) - [Fact] - public async Task Performance_CacheOperationsHaveLowOverhead() - { - // Arrange - var cacheManager = new CacheManager(_memoryCache, null, _loggerMock.Object); - const int iterations = 1000; - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - - // Act - for (int i = 0; i < iterations; i++) - { - var key = $"perf-key-{i}"; - await cacheManager.SetAsync(key, i, CacheRegion.Default); - var value = await cacheManager.GetAsync(key, CacheRegion.Default); - Assert.Equal(i, value); - } - - stopwatch.Stop(); - - // Assert - var avgOperationTime = stopwatch.ElapsedMilliseconds / (double)(iterations * 2); // Set + Get - Assert.True(avgOperationTime < 1.0, $"Average operation time {avgOperationTime}ms exceeds 1ms threshold"); - } - - - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/CostCalculationServiceAdditionalTests.EdgeCases.cs b/ConduitLLM.Tests/Core/Services/CostCalculationServiceAdditionalTests.EdgeCases.cs deleted file mode 100644 index efc6bb833..000000000 --- a/ConduitLLM.Tests/Core/Services/CostCalculationServiceAdditionalTests.EdgeCases.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System.Text.Json; -using ConduitLLM.Core.Models; -using ConduitLLM.Tests.TestHelpers; -using FluentAssertions; -using Moq; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class CostCalculationServiceAdditionalTests - { - #region Edge Cases for CalculateCostAsync - - [Fact] - public async Task CalculateCostAsync_WithVideoButNoVideoCost_IgnoresVideoUsage() - { - // Arrange - var modelId = "text-only-model"; - var usage = new Usage - { - PromptTokens = 100, - CompletionTokens = 50, - TotalTokens = 150, - VideoDurationSeconds = 10.0, // Has video duration but model doesn't support video - VideoResolution = "1920x1080" - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m, - VideoCostPerSecond = null // No video cost defined - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Should only calculate text costs: (100 * 10.00 / 1_000_000) + (50 * 20.00 / 1_000_000) = 0.001 + 0.001 = 0.002 - result.Should().Be(0.002m); - } - - [Fact] - public async Task CalculateCostAsync_WithImageButNoImageCost_IgnoresImageUsage() - { - // Arrange - var modelId = "text-only-model"; - var usage = new Usage - { - PromptTokens = 100, - CompletionTokens = 50, - TotalTokens = 150, - ImageCount = 5 // Has images but model doesn't support image generation - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m, - ImageCostPerImage = null // No image cost defined - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Should only calculate text costs: (100 * 10.00 / 1_000_000) + (50 * 20.00 / 1_000_000) = 0.001 + 0.001 = 0.002 - result.Should().Be(0.002m); - } - - [Fact] - public async Task CalculateCostAsync_WithEmptyVideoResolution_UsesBaseCost() - { - // Arrange - var modelId = "video/model"; - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = 10, - VideoResolution = "" // Empty string resolution - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - VideoCostPerSecond = 0.1m, - VideoResolutionMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["1920x1080"] = 1.5m - // No multiplier for empty string - }) - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: 10 * 0.1 = 1.0 (no multiplier applied for empty resolution) - result.Should().Be(1.0m); - } - - [Fact] - public async Task CalculateCostAsync_WithZeroVideoDuration_ReturnsZeroVideoCost() - { - // Arrange - var modelId = "video/model"; - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = 0, // Zero duration - VideoResolution = "1920x1080" - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - VideoCostPerSecond = 0.1m, - VideoResolutionMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["1920x1080"] = 1.5m - }) - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: 0 * 0.1 * 1.5 = 0 - result.Should().Be(0m); - } - - [Fact] - public async Task CalculateCostAsync_WithEmbeddingCostButNoEmbeddingCostDefined_UsesInputCost() - { - // Arrange - var modelId = "model-without-embedding-cost"; - var usage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 0, // No completions (typical for embeddings) - TotalTokens = 1000 - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m, - EmbeddingCostPerMillionTokens = null // No embedding cost defined - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Should use input cost: 1000 * 0.00001 = 0.01 - result.Should().Be(0.01m); - } - - [Fact] - public async Task CalculateCostAsync_WithFractionalVideoSeconds_CalculatesPrecisely() - { - // Arrange - var modelId = "video/model"; - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = 3.14159265359, // Pi seconds - VideoResolution = "1280x720" - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - VideoCostPerSecond = 0.01m, - VideoResolutionMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["1280x720"] = 1.0m - }) - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: 3.14159265359 * 0.01 * 1.0 = 0.0314159265359 - result.Should().Be(0.0314159265359m); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/CostCalculationServiceAdditionalTests.Refund.cs b/ConduitLLM.Tests/Core/Services/CostCalculationServiceAdditionalTests.Refund.cs deleted file mode 100644 index fd678ef50..000000000 --- a/ConduitLLM.Tests/Core/Services/CostCalculationServiceAdditionalTests.Refund.cs +++ /dev/null @@ -1,449 +0,0 @@ -using System.Text.Json; -using ConduitLLM.Core.Models; -using ConduitLLM.Tests.TestHelpers; -using FluentAssertions; -using Moq; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class CostCalculationServiceAdditionalTests - { - #region Refund Method Additional Tests - - [Fact] - public async Task CalculateRefundAsync_WithZeroRefundUsage_ReturnsZeroRefund() - { - // Arrange - var modelId = "openai/gpt-4o"; - var originalUsage = new Usage { PromptTokens = 1000, CompletionTokens = 500, TotalTokens = 1500 }; - var refundUsage = new Usage { PromptTokens = 0, CompletionTokens = 0, TotalTokens = 0 }; - var refundReason = "No actual usage to refund"; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 30.00m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, refundReason); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(0m); - result.Breakdown.Should().NotBeNull(); - result.Breakdown!.InputTokenRefund.Should().Be(0m); - result.Breakdown.OutputTokenRefund.Should().Be(0m); - } - - [Fact] - public async Task CalculateRefundAsync_WithNullOriginalTransactionId_HandlesGracefully() - { - // Arrange - var modelId = "openai/gpt-4o"; - var originalUsage = new Usage { PromptTokens = 1000, CompletionTokens = 500, TotalTokens = 1500 }; - var refundUsage = new Usage { PromptTokens = 500, CompletionTokens = 250, TotalTokens = 750 }; - var refundReason = "Service interruption"; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 30.00m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, refundReason, null); - - // Assert - result.Should().NotBeNull(); - result.OriginalTransactionId.Should().BeNull(); - result.RefundAmount.Should().Be(0.0125m); - } - - [Fact] - public async Task CalculateRefundAsync_WithVideoRefundNoMultipliers_UsesBaseCost() - { - // Arrange - var modelId = "video-model"; - var originalUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = 20.0, - VideoResolution = "1920x1080" - }; - var refundUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = 10.0, - VideoResolution = "1920x1080" - }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - VideoCostPerSecond = 0.2m, - VideoResolutionMultipliers = null // No multipliers - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Video processing error"); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(2.0m); // 10.0 * 0.2 - result.Breakdown!.VideoRefund.Should().Be(2.0m); - } - - [Fact] - public async Task CalculateRefundAsync_WithUnknownVideoResolution_UsesBaseCost() - { - // Arrange - var modelId = "video-model"; - var originalUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = 10.0, - VideoResolution = "4K-UHD" - }; - var refundUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = 5.0, - VideoResolution = "4K-UHD" // Unknown resolution - }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - VideoCostPerSecond = 0.1m, - VideoResolutionMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["1920x1080"] = 1.5m, - ["1280x720"] = 1.0m - // 4K-UHD not in dictionary - }) - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Video quality issue"); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(0.5m); // 5.0 * 0.1 (no multiplier) - result.Breakdown!.VideoRefund.Should().Be(0.5m); - } - - [Fact] - public async Task CalculateRefundAsync_WithNegativeImageCount_ReturnsValidationError() - { - // Arrange - var modelId = "openai/dall-e-3"; - var originalUsage = new Usage { PromptTokens = 0, CompletionTokens = 0, TotalTokens = 0, ImageCount = 5 }; - var refundUsage = new Usage { PromptTokens = 0, CompletionTokens = 0, TotalTokens = 0, ImageCount = -2 }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - ImageCostPerImage = 0.04m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Invalid refund test"); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(0); - result.ValidationMessages.Should().Contain("Refund image count must be non-negative."); - } - - [Fact] - public async Task CalculateRefundAsync_WithNegativeVideoDuration_ReturnsValidationError() - { - // Arrange - var modelId = "video-model"; - var originalUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = 10.0 - }; - var refundUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = -5.0 - }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - VideoCostPerSecond = 0.1m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Invalid video refund"); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(0); - result.ValidationMessages.Should().Contain("Refund video duration must be non-negative."); - } - - [Fact] - public async Task CalculateRefundAsync_WithMismatchedVideoResolution_StillCalculates() - { - // Arrange - refund uses different resolution than original - var modelId = "video-model"; - var originalUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = 10.0, - VideoResolution = "1920x1080" - }; - var refundUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = 5.0, - VideoResolution = "1280x720" // Different resolution - }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - VideoCostPerSecond = 0.1m, - VideoResolutionMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["1920x1080"] = 1.5m, - ["1280x720"] = 1.0m - }) - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Resolution mismatch refund"); - - // Assert - result.Should().NotBeNull(); - // Uses the refund's resolution multiplier: 5.0 * 0.1 * 1.0 = 0.5 - result.RefundAmount.Should().Be(0.5m); - result.Breakdown!.VideoRefund.Should().Be(0.5m); - } - - [Fact] - public async Task CalculateRefundAsync_WithOnlyInputTokenRefund_NoCompletionRefund() - { - // Arrange - var modelId = "openai/gpt-4o"; - var originalUsage = new Usage { PromptTokens = 1000, CompletionTokens = 500, TotalTokens = 1500 }; - var refundUsage = new Usage { PromptTokens = 600, CompletionTokens = 0, TotalTokens = 600 }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 30.00m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Partial input refund"); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(0.006m); // 600 * 0.00001 - result.Breakdown!.InputTokenRefund.Should().Be(0.006m); - result.Breakdown.OutputTokenRefund.Should().Be(0m); - } - - [Fact] - public async Task CalculateRefundAsync_WithOnlyOutputTokenRefund_NoInputRefund() - { - // Arrange - var modelId = "openai/gpt-4o"; - var originalUsage = new Usage { PromptTokens = 1000, CompletionTokens = 500, TotalTokens = 1500 }; - var refundUsage = new Usage { PromptTokens = 0, CompletionTokens = 300, TotalTokens = 300 }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 30.00m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Partial output refund"); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(0.009m); // 300 * 0.00003 - result.Breakdown!.InputTokenRefund.Should().Be(0m); - result.Breakdown.OutputTokenRefund.Should().Be(0.009m); - } - - [Fact] - public async Task CalculateRefundAsync_WithCancellationToken_PropagatesToken() - { - // Arrange - var modelId = "openai/gpt-4o"; - var originalUsage = new Usage { PromptTokens = 1000, CompletionTokens = 500, TotalTokens = 1500 }; - var refundUsage = new Usage { PromptTokens = 500, CompletionTokens = 250, TotalTokens = 750 }; - using var cts = new CancellationTokenSource(); - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 30.00m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, cts.Token)) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Test refund", null, cts.Token); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(0.0125m); - _modelCostServiceMock.Verify(x => x.GetCostForModelAsync(modelId, cts.Token), Times.Once); - } - - [Fact] - public async Task CalculateRefundAsync_WithRegularCostWhenEmbeddingCostExists_UsesInputCost() - { - // Arrange - Model has embedding cost but usage has completion tokens (not an embedding request) - var modelId = "multimodal/model"; - var originalUsage = new Usage { PromptTokens = 1000, CompletionTokens = 500, TotalTokens = 1500 }; - var refundUsage = new Usage { PromptTokens = 500, CompletionTokens = 250, TotalTokens = 750 }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, // Regular input cost - OutputCostPerMillionTokens = 30.00m, // Regular output cost - EmbeddingCostPerMillionTokens = 0.10m // Embedding cost (not used here) - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Regular refund"); - - // Assert - result.Should().NotBeNull(); - // Should use regular costs: (500 * 0.00001) + (250 * 0.00003) = 0.005 + 0.0075 = 0.0125 - result.RefundAmount.Should().Be(0.0125m); - result.Breakdown!.InputTokenRefund.Should().Be(0.005m); - result.Breakdown.OutputTokenRefund.Should().Be(0.0075m); - result.Breakdown.EmbeddingRefund.Should().Be(0m); - } - - [Theory] - [InlineData(0, 0, 0, 0, 0)] // All zero - [InlineData(100, 50, 50, 25, 0.00125)] // Exact half refund - [InlineData(1000, 500, 1000, 500, 0.025)] // Full refund - [InlineData(2000, 1000, 100, 50, 0.0025)] // Small partial refund - public async Task CalculateRefundAsync_WithVariousScenarios_CalculatesCorrectly( - int originalPrompt, int originalCompletion, int refundPrompt, int refundCompletion, decimal expectedRefund) - { - // Arrange - var modelId = "test/model"; - var originalUsage = new Usage - { - PromptTokens = originalPrompt, - CompletionTokens = originalCompletion, - TotalTokens = originalPrompt + originalCompletion - }; - var refundUsage = new Usage - { - PromptTokens = refundPrompt, - CompletionTokens = refundCompletion, - TotalTokens = refundPrompt + refundCompletion - }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 30.00m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Test scenario"); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(expectedRefund); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/CostCalculationServiceBasicTests.BasicCalculations.cs b/ConduitLLM.Tests/Core/Services/CostCalculationServiceBasicTests.BasicCalculations.cs deleted file mode 100644 index 40e07932a..000000000 --- a/ConduitLLM.Tests/Core/Services/CostCalculationServiceBasicTests.BasicCalculations.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System.Text.Json; -using ConduitLLM.Core.Models; -using ConduitLLM.Tests.TestHelpers; -using FluentAssertions; -using Moq; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class CostCalculationServiceBasicTests - { - [Fact] - public async Task CalculateCostAsync_WithTextGeneration_CalculatesCorrectly() - { - // Arrange - var modelId = "openai/gpt-4o"; - var usage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 500, - TotalTokens = 1500 - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, // $10 per million tokens - OutputCostPerMillionTokens = 30.00m // $30 per million tokens - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: (1000 * 10.00 / 1_000_000) + (500 * 30.00 / 1_000_000) = 0.01 + 0.015 = 0.025 - result.Should().Be(0.025m); - } - - [Fact] - public async Task CalculateCostAsync_WithEmbedding_UsesEmbeddingCost() - { - // Arrange - var modelId = "openai/text-embedding-ada-002"; - var usage = new Usage - { - PromptTokens = 2000, - CompletionTokens = 0, - TotalTokens = 2000 - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 10.00m, - EmbeddingCostPerMillionTokens = 0.10m // $0.10 per million tokens - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: 2000 * 0.10 / 1_000_000 = 0.0002 - result.Should().Be(0.0002m); - } - - [Fact] - public async Task CalculateCostAsync_WithImageGeneration_CalculatesCorrectly() - { - // Arrange - var modelId = "openai/dall-e-3"; - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - ImageCount = 3 - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - ImageCostPerImage = 0.04m // $0.04 per image - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: 3 * 0.04 = 0.12 - result.Should().Be(0.12m); - } - - [Fact] - public async Task CalculateCostAsync_WithVideoGeneration_CalculatesCorrectly() - { - // Arrange - var modelId = "runway/gen-2"; - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = 4.0, - VideoResolution = "1920x1080" - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - VideoCostPerSecond = 0.5m, // $0.50 per second - VideoResolutionMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["1920x1080"] = 1.2m - }) - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: 4.0 * 0.5 * 1.2 = 2.4 - result.Should().Be(2.4m); - } - - [Fact] - public async Task CalculateCostAsync_WithCombinedUsage_CalculatesAllComponents() - { - // Arrange - var modelId = "multimodal/gpt-4-vision"; - var usage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 500, - TotalTokens = 1500, - ImageCount = 2, - VideoDurationSeconds = 2.0, - VideoResolution = "1280x720" - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 30.00m, - ImageCostPerImage = 0.01275m, - VideoCostPerSecond = 0.2m, - VideoResolutionMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["1280x720"] = 1.0m - }) - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: - // Text: (1000 * 10 / 1M) + (500 * 30 / 1M) = 0.01 + 0.015 = 0.025 - // Images: 2 * 0.01275 = 0.0255 - // Video: 2.0 * 0.2 * 1.0 = 0.4 - // Total: 0.025 + 0.0255 + 0.4 = 0.4505 - result.Should().Be(0.4505m); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/CostCalculationServiceBatchTests.cs b/ConduitLLM.Tests/Core/Services/CostCalculationServiceBatchTests.cs deleted file mode 100644 index 26a1024ea..000000000 --- a/ConduitLLM.Tests/Core/Services/CostCalculationServiceBatchTests.cs +++ /dev/null @@ -1,353 +0,0 @@ -using System.Text.Json; -using ConduitLLM.Core.Models; -using FluentAssertions; -using Moq; -using Xunit.Abstractions; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Core.Services -{ - /// - /// Tests for batch processing functionality - /// - public class CostCalculationServiceBatchTests : CostCalculationServiceTestBase - { - public CostCalculationServiceBatchTests(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public async Task CalculateCostAsync_WithBatchProcessing_AppliesDiscount() - { - // Arrange - var modelId = "openai/gpt-4o"; - var usage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 500, - TotalTokens = 1500, - IsBatch = true - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 1000.00m, - OutputCostPerMillionTokens = 2000.00m, - SupportsBatchProcessing = true, - BatchProcessingMultiplier = 0.5m // 50% discount - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected without batch: (1000 * 1000.00 / 1_000_000) + (500 * 2000.00 / 1_000_000) = 1.0 + 1.0 = 2.0 - // Expected with 50% batch discount: 2.0 * 0.5 = 1.0 - result.Should().Be(1.0m); - } - - [Fact] - public async Task CalculateCostAsync_WithBatchProcessingButNotSupported_NoDiscount() - { - // Arrange - var modelId = "openai/gpt-3.5"; - var usage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 500, - TotalTokens = 1500, - IsBatch = true - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 1000.00m, - OutputCostPerMillionTokens = 2000.00m, - SupportsBatchProcessing = false, // Model doesn't support batch - BatchProcessingMultiplier = 0.5m - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: No discount applied since model doesn't support batch - // (1000 * 1000.00 / 1_000_000) + (500 * 2000.00 / 1_000_000) = 1.0 + 1.0 = 2.0 - result.Should().Be(2.0m); - } - - [Fact] - public async Task CalculateCostAsync_WithBatchFalse_NoDiscount() - { - // Arrange - var modelId = "openai/gpt-4o"; - var usage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 500, - TotalTokens = 1500, - IsBatch = false - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 1000.00m, - OutputCostPerMillionTokens = 2000.00m, - SupportsBatchProcessing = true, - BatchProcessingMultiplier = 0.5m - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: No discount since IsBatch is false - // (1000 * 1000.00 / 1_000_000) + (500 * 2000.00 / 1_000_000) = 1.0 + 1.0 = 2.0 - result.Should().Be(2.0m); - } - - [Fact] - public async Task CalculateCostAsync_WithBatchNullMultiplier_NoDiscount() - { - // Arrange - var modelId = "openai/gpt-4o"; - var usage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 500, - TotalTokens = 1500, - IsBatch = true - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 1000.00m, - OutputCostPerMillionTokens = 2000.00m, - SupportsBatchProcessing = true, - BatchProcessingMultiplier = null // No multiplier defined - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: No discount since multiplier is null - // (1000 * 1000.00 / 1_000_000) + (500 * 2000.00 / 1_000_000) = 1.0 + 1.0 = 2.0 - result.Should().Be(2.0m); - } - - [Fact] - public async Task CalculateCostAsync_WithBatchAndMultiModalUsage_AppliesDiscountToAll() - { - // Arrange - var modelId = "multimodal/model"; - var usage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 500, - TotalTokens = 1500, - ImageCount = 2, - VideoDurationSeconds = 3, - VideoResolution = "1280x720", - IsBatch = true - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m, - ImageCostPerImage = 0.05m, - VideoCostPerSecond = 0.1m, - VideoResolutionMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["1280x720"] = 0.8m - }), - SupportsBatchProcessing = true, - BatchProcessingMultiplier = 0.6m // 40% discount - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected without batch: - // Text: (1000 * 10.00 / 1_000_000) + (500 * 20.00 / 1_000_000) = 0.01 + 0.01 = 0.02 - // Images: 2 * 0.05 = 0.1 - // Video: 3 * 0.1 * 0.8 = 0.24 - // Total before batch: 0.02 + 0.1 + 0.24 = 0.36 - // With 40% discount (0.6 multiplier): 0.36 * 0.6 = 0.216 - result.Should().Be(0.216m); - } - - [Theory] - [InlineData(0.5, 1.0)] // 50% discount - [InlineData(0.6, 1.2)] // 40% discount - [InlineData(0.4, 0.8)] // 60% discount - [InlineData(1.0, 2.0)] // No discount - public async Task CalculateCostAsync_WithVariousBatchMultipliers_AppliesCorrectDiscount(decimal multiplier, decimal expectedCost) - { - // Arrange - var modelId = "openai/gpt-4o"; - var usage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 500, - TotalTokens = 1500, - IsBatch = true - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 1000.00m, - OutputCostPerMillionTokens = 2000.00m, - SupportsBatchProcessing = true, - BatchProcessingMultiplier = multiplier - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - result.Should().Be(expectedCost); - } - - [Fact] - public async Task CalculateCostAsync_WithCachedTokensAndBatchProcessing_AppliesBothDiscounts() - { - // Arrange - var modelId = "anthropic/claude-3-haiku"; - var usage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 500, - TotalTokens = 1500, - CachedInputTokens = 600, - IsBatch = true - }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 30.00m, - CachedInputCostPerMillionTokens = 1.00m, - SupportsBatchProcessing = true, - BatchProcessingMultiplier = 0.5m // 50% discount for batch - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Regular input: 400 * 10.00 / 1_000_000 = 0.004 - // Cached input: 600 * 1.00 / 1_000_000 = 0.0006 - // Output: 500 * 30.00 / 1_000_000 = 0.015 - // Subtotal: 0.004 + 0.0006 + 0.015 = 0.0196 - // With batch discount: 0.0196 * 0.5 = 0.0098 - result.Should().Be(0.0098m); - } - - [Fact] - public async Task CalculateCostAsync_WithSearchUnitsAndBatchProcessing_AppliesDiscountToAll() - { - // Arrange - var modelId = "cohere/rerank-3.5"; - var usage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 0, - TotalTokens = 1000, - SearchUnits = 100, - IsBatch = true - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 0m, - CostPerSearchUnit = 2.0m, - SupportsBatchProcessing = true, - BatchProcessingMultiplier = 0.5m // 50% discount - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Token cost: 1000 * 10.00 / 1_000_000 = 0.01 - // Search unit cost: 100 * (2.0 / 1000) = 0.2 - // Total before discount: 0.01 + 0.2 = 0.21 - // After 50% discount: 0.21 * 0.5 = 0.105 - result.Should().Be(0.105m); - } - - [Fact] - public async Task CalculateCostAsync_WithInferenceStepsAndBatchProcessing_AppliesDiscountToAll() - { - // Arrange - var modelId = "fireworks/batch-model"; - var usage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 500, - TotalTokens = 1500, - InferenceSteps = 10, - IsBatch = true - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m, - CostPerInferenceStep = 0.0002m, - SupportsBatchProcessing = true, - BatchProcessingMultiplier = 0.5m // 50% discount - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Token cost: (1000 * 10.00 / 1_000_000) + (500 * 20.00 / 1_000_000) = 0.01 + 0.01 = 0.02 - // Step cost: 10 * 0.0002 = 0.002 - // Total before discount: 0.022 - // After 50% discount: 0.011 - result.Should().Be(0.011m); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/CostCalculationServiceImageQualityTests.cs b/ConduitLLM.Tests/Core/Services/CostCalculationServiceImageQualityTests.cs deleted file mode 100644 index 2132de37e..000000000 --- a/ConduitLLM.Tests/Core/Services/CostCalculationServiceImageQualityTests.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System.Text.Json; -using ConduitLLM.Core.Models; -using FluentAssertions; -using Moq; -using Xunit.Abstractions; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Core.Services -{ - /// - /// Tests for image quality multiplier functionality - /// - public class CostCalculationServiceImageQualityTests : CostCalculationServiceTestBase - { - public CostCalculationServiceImageQualityTests(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public async Task CalculateCostAsync_WithImageQualityMultiplier_AppliesMultiplier() - { - // Arrange - var modelId = "openai/dall-e-3"; - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - ImageCount = 2, - ImageQuality = "hd" - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - ImageCostPerImage = 0.04m, // Standard quality price - ImageQualityMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["standard"] = 1.0m, - ["hd"] = 2.0m - }) - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: 2 images * 0.04 base cost * 2.0 HD multiplier = 0.16 - result.Should().Be(0.16m); - } - - [Fact] - public async Task CalculateCostAsync_WithStandardQuality_UsesDefaultMultiplier() - { - // Arrange - var modelId = "openai/dall-e-3"; - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - ImageCount = 3, - ImageQuality = "standard" - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - ImageCostPerImage = 0.04m, - ImageQualityMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["standard"] = 1.0m, - ["hd"] = 2.0m - }) - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: 3 images * 0.04 base cost * 1.0 standard multiplier = 0.12 - result.Should().Be(0.12m); - } - - [Fact] - public async Task CalculateCostAsync_WithNoImageQuality_UsesBasePrice() - { - // Arrange - var modelId = "openai/dall-e-2"; - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - ImageCount = 1, - ImageQuality = null // No quality specified - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - ImageCostPerImage = 0.02m, - ImageQualityMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["standard"] = 1.0m, - ["hd"] = 2.0m - }) - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: 1 image * 0.02 base cost (no multiplier applied) - result.Should().Be(0.02m); - } - - [Fact] - public async Task CalculateCostAsync_WithUnknownQuality_UsesBasePrice() - { - // Arrange - var modelId = "openai/dall-e-3"; - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - ImageCount = 2, - ImageQuality = "ultra" // Quality not in multipliers - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - ImageCostPerImage = 0.04m, - ImageQualityMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["standard"] = 1.0m, - ["hd"] = 2.0m - }) - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: 2 images * 0.04 base cost (no multiplier found) - result.Should().Be(0.08m); - } - - [Fact] - public async Task CalculateCostAsync_WithCaseInsensitiveQuality_AppliesMultiplier() - { - // Arrange - var modelId = "openai/dall-e-3"; - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - ImageCount = 1, - ImageQuality = "HD" // Uppercase - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - ImageCostPerImage = 0.04m, - ImageQualityMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["standard"] = 1.0m, - ["hd"] = 2.0m // Lowercase in dictionary - }) - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: 1 image * 0.04 base cost * 2.0 HD multiplier = 0.08 - result.Should().Be(0.08m); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/CostCalculationServiceRefundTests.Advanced.cs b/ConduitLLM.Tests/Core/Services/CostCalculationServiceRefundTests.Advanced.cs deleted file mode 100644 index 189a7af1b..000000000 --- a/ConduitLLM.Tests/Core/Services/CostCalculationServiceRefundTests.Advanced.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System.Text.Json; -using ConduitLLM.Core.Models; -using FluentAssertions; -using Moq; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Core.Services -{ - /// - /// Advanced refund calculation tests (cached tokens, batch processing, quality multipliers, validation) - /// - public partial class CostCalculationServiceRefundTests - { - [Fact] - public async Task CalculateRefundAsync_WithNegativeValues_ReturnsValidationError() - { - // Arrange - var modelId = "openai/gpt-4o"; - var originalUsage = new Usage { PromptTokens = 1000, CompletionTokens = 500, TotalTokens = 1500 }; - var refundUsage = new Usage { PromptTokens = -100, CompletionTokens = -50, TotalTokens = -150 }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 30.00m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Invalid refund test"); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(0); - result.ValidationMessages.Should().Contain("Refund token counts must be non-negative."); - } - - [Fact] - public async Task CalculateRefundAsync_WithModelNotFound_ReturnsValidationMessage() - { - // Arrange - var modelId = "non-existent-model"; - var originalUsage = new Usage { PromptTokens = 1000, CompletionTokens = 500, TotalTokens = 1500 }; - var refundUsage = new Usage { PromptTokens = 500, CompletionTokens = 250, TotalTokens = 750 }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync((ModelCost?)null); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Test refund"); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(0); - result.ValidationMessages.Should().Contain($"Cost information not found for model {modelId}."); - } - - [Fact] - public async Task CalculateRefundAsync_WithCachedTokens_CalculatesCorrectRefund() - { - // Arrange - var modelId = "google/gemini-1.5-flash"; - var originalUsage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 500, - TotalTokens = 1500, - CachedInputTokens = 400, - CachedWriteTokens = 200 - }; - var refundUsage = new Usage - { - PromptTokens = 500, - CompletionTokens = 250, - TotalTokens = 750, - CachedInputTokens = 200, - CachedWriteTokens = 100 - }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 30.00m, - CachedInputCostPerMillionTokens = 1.00m, - CachedInputWriteCostPerMillionTokens = 25.00m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Partial service interruption"); - - // Assert - // Regular input refund: 300 * 0.00001 = 0.003 (500 total - 200 cached = 300 regular) - // Cached input refund: 200 * 0.000001 = 0.0002 - // Cache write refund: 100 * 0.000025 = 0.0025 - // Output refund: 250 * 0.00003 = 0.0075 - // Total refund: 0.003 + 0.0002 + 0.0025 + 0.0075 = 0.0132 - result.RefundAmount.Should().Be(0.0132m); - result.IsPartialRefund.Should().BeFalse(); - } - - [Fact] - public async Task CalculateRefundAsync_WithBatchProcessing_AppliesDiscountToRefund() - { - // Arrange - var modelId = "openai/gpt-4o"; - var originalUsage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 500, - TotalTokens = 1500, - IsBatch = true - }; - var refundUsage = new Usage - { - PromptTokens = 500, - CompletionTokens = 200, - TotalTokens = 700, - IsBatch = true - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 1000.00m, - OutputCostPerMillionTokens = 2000.00m, - SupportsBatchProcessing = true, - BatchProcessingMultiplier = 0.5m // 50% discount - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, - originalUsage, - refundUsage, - "Test refund", - "transaction-123" - ); - - // Assert - result.Should().NotBeNull(); - result.ValidationMessages.Should().BeEmpty(); - // Expected refund without batch: (500 * 0.001) + (200 * 0.002) = 0.5 + 0.4 = 0.9 - // Expected with 50% batch discount: 0.9 * 0.5 = 0.45 - result.RefundAmount.Should().Be(0.45m); - result.Breakdown.Should().NotBeNull(); - } - - [Fact] - public async Task CalculateRefundAsync_WithImageQualityMultiplier_AppliesMultiplierToRefund() - { - // Arrange - var modelId = "openai/dall-e-3"; - var originalUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - ImageCount = 5, - ImageQuality = "hd" - }; - var refundUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - ImageCount = 2, - ImageQuality = "hd" - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - ImageCostPerImage = 0.04m, - ImageQualityMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["standard"] = 1.0m, - ["hd"] = 2.0m - }) - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, - originalUsage, - refundUsage, - "Quality issue with generated images", - "transaction-456" - ); - - // Assert - result.Should().NotBeNull(); - result.ValidationMessages.Should().BeEmpty(); - // Expected refund: 2 images * 0.04 base cost * 2.0 HD multiplier = 0.16 - result.RefundAmount.Should().Be(0.16m); - result.Breakdown.Should().NotBeNull(); - result.Breakdown.ImageRefund.Should().Be(0.16m); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/CostCalculationServiceRefundTests.Media.cs b/ConduitLLM.Tests/Core/Services/CostCalculationServiceRefundTests.Media.cs deleted file mode 100644 index b0a64e2cc..000000000 --- a/ConduitLLM.Tests/Core/Services/CostCalculationServiceRefundTests.Media.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System.Text.Json; -using ConduitLLM.Core.Models; -using FluentAssertions; -using Moq; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Core.Services -{ - /// - /// Media-related refund calculation tests (images, video) - /// - public partial class CostCalculationServiceRefundTests - { - [Fact] - public async Task CalculateRefundAsync_WithImageRefund_CalculatesCorrectly() - { - // Arrange - var modelId = "openai/dall-e-3"; - var originalUsage = new Usage { PromptTokens = 0, CompletionTokens = 0, TotalTokens = 0, ImageCount = 5 }; - var refundUsage = new Usage { PromptTokens = 0, CompletionTokens = 0, TotalTokens = 0, ImageCount = 2 }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - ImageCostPerImage = 0.04m // $0.04 per image - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Image generation failure"); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(0.08m); // 2 * 0.04 - result.Breakdown!.ImageRefund.Should().Be(0.08m); - } - - [Fact] - public async Task CalculateRefundAsync_WithVideoRefund_IncludesResolutionMultiplier() - { - // Arrange - var modelId = "some-video-model"; - var originalUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = 10.0, - VideoResolution = "1920x1080" - }; - var refundUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = 5.0, - VideoResolution = "1920x1080" - }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - VideoCostPerSecond = 0.1m, - VideoResolutionMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["1920x1080"] = 1.5m - }) - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Video processing error"); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(0.75m); // 5.0 * 0.1 * 1.5 - result.Breakdown!.VideoRefund.Should().Be(0.75m); - } - - [Fact] - public async Task CalculateRefundAsync_WithMixedUsageRefund_CalculatesAllComponents() - { - // Arrange - var modelId = "multimodal-model"; - var originalUsage = new Usage - { - PromptTokens = 1000, - CompletionTokens = 500, - TotalTokens = 1500, - ImageCount = 3, - VideoDurationSeconds = 10.0, - VideoResolution = "1280x720" - }; - var refundUsage = new Usage - { - PromptTokens = 500, - CompletionTokens = 250, - TotalTokens = 750, - ImageCount = 1, - VideoDurationSeconds = 5.0, - VideoResolution = "1280x720" - }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 30.00m, - ImageCostPerImage = 0.04m, - VideoCostPerSecond = 0.1m, - VideoResolutionMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["1280x720"] = 1.0m - }) - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Partial service failure"); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(0.5525m); // 0.005 + 0.0075 + 0.04 + 0.5 - result.Breakdown!.InputTokenRefund.Should().Be(0.005m); - result.Breakdown.OutputTokenRefund.Should().Be(0.0075m); - result.Breakdown.ImageRefund.Should().Be(0.04m); - result.Breakdown.VideoRefund.Should().Be(0.5m); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/CostCalculationServiceRefundTests.Specialized.cs b/ConduitLLM.Tests/Core/Services/CostCalculationServiceRefundTests.Specialized.cs deleted file mode 100644 index 11b23cb68..000000000 --- a/ConduitLLM.Tests/Core/Services/CostCalculationServiceRefundTests.Specialized.cs +++ /dev/null @@ -1,243 +0,0 @@ -using ConduitLLM.Core.Models; -using FluentAssertions; -using Moq; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Core.Services -{ - /// - /// Specialized refund calculation tests (embedding, search units, inference steps) - /// - public partial class CostCalculationServiceRefundTests - { - [Fact] - public async Task CalculateRefundAsync_WithEmbeddingRefund_CalculatesCorrectly() - { - // Arrange - var modelId = "openai/text-embedding-ada-002"; - var originalUsage = new Usage { PromptTokens = 5000, CompletionTokens = 0, TotalTokens = 5000 }; - var refundUsage = new Usage { PromptTokens = 2000, CompletionTokens = 0, TotalTokens = 2000 }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 0m, - EmbeddingCostPerMillionTokens = 100.00m // $0.0001 per token - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Embedding service error"); - - // Assert - result.Should().NotBeNull(); - result.RefundAmount.Should().Be(0.2m); // 2000 * 0.0001 - result.Breakdown!.EmbeddingRefund.Should().Be(0.2m); - } - - [Fact] - public async Task CalculateRefundAsync_WithEmbeddingAndImages_UsesEmbeddingCost() - { - // Arrange - var modelId = "openai/multimodal-embed"; - var originalUsage = new Usage { PromptTokens = 5000, CompletionTokens = 0, TotalTokens = 5000, ImageCount = 3 }; - var refundUsage = new Usage { PromptTokens = 2000, CompletionTokens = 0, TotalTokens = 2000, ImageCount = 1 }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 100.00m, // Regular cost (expensive) - OutputCostPerMillionTokens = 0m, - EmbeddingCostPerMillionTokens = 10.00m, // Embedding cost (10x cheaper) - ImageCostPerImage = 0.02m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Partial refund for embedding with images"); - - // Assert - result.Should().NotBeNull(); - // Embedding refund: 2000 * 0.00001 = 0.02 - // Image refund: 1 * 0.02 = 0.02 - // Total: 0.04 - result.RefundAmount.Should().Be(0.04m); - result.Breakdown!.EmbeddingRefund.Should().Be(0.02m); - result.Breakdown.ImageRefund.Should().Be(0.02m); - result.Breakdown.InputTokenRefund.Should().Be(0m); // Should not use input token cost - } - - [Fact] - public async Task CalculateRefundAsync_WithSearchUnits_CalculatesCorrectRefund() - { - // Arrange - var modelId = "cohere/rerank-3.5"; - var originalUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - SearchUnits = 50 - }; - var refundUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - SearchUnits = 20 - }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - CostPerSearchUnit = 2.0m // $2.00 per 1K search units - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Service interruption"); - - // Assert - // Expected refund: 20 * (2.0 / 1000) = 20 * 0.002 = 0.04 - result.RefundAmount.Should().Be(0.04m); - result.Breakdown!.SearchUnitRefund.Should().Be(0.04m); - result.IsPartialRefund.Should().BeFalse(); - } - - [Fact] - public async Task CalculateRefundAsync_WithSearchUnitsExceedingOriginal_ReportsValidationError() - { - // Arrange - var modelId = "cohere/rerank-3.5"; - var originalUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - SearchUnits = 20 - }; - var refundUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - SearchUnits = 30 // More than original - }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - CostPerSearchUnit = 2.0m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Invalid refund request"); - - // Assert - result.IsPartialRefund.Should().BeTrue(); - result.ValidationMessages.Should().Contain(m => m.Contains("Refund search units")); - } - - [Fact] - public async Task CalculateRefundAsync_WithInferenceSteps_CalculatesCorrectRefund() - { - // Arrange - var modelId = "fireworks/flux-pro"; - var originalUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - ImageCount = 3, - InferenceSteps = 20 - }; - var refundUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - ImageCount = 1, - InferenceSteps = 20 - }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - CostPerInferenceStep = 0.0005m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Partial image generation failure"); - - // Assert - // Expected refund: 20 * 0.0005 = 0.01 - result.RefundAmount.Should().Be(0.01m); - result.Breakdown!.InferenceStepRefund.Should().Be(0.01m); - result.IsPartialRefund.Should().BeFalse(); - } - - [Fact] - public async Task CalculateRefundAsync_WithInferenceStepsExceedingOriginal_ReportsValidationError() - { - // Arrange - var modelId = "fireworks/sdxl"; - var originalUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - InferenceSteps = 30 - }; - var refundUsage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - InferenceSteps = 50 // More than original - }; - - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - CostPerInferenceStep = 0.00013m - }; - - _modelCostServiceMock.Setup(m => m.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateRefundAsync( - modelId, originalUsage, refundUsage, "Invalid refund request"); - - // Assert - result.IsPartialRefund.Should().BeTrue(); - result.ValidationMessages.Should().Contain(m => m.Contains("Refund inference steps")); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/CostCalculationServiceSearchAndInferenceTests.InferenceSteps.cs b/ConduitLLM.Tests/Core/Services/CostCalculationServiceSearchAndInferenceTests.InferenceSteps.cs deleted file mode 100644 index 222dc46b2..000000000 --- a/ConduitLLM.Tests/Core/Services/CostCalculationServiceSearchAndInferenceTests.InferenceSteps.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System.Text.Json; -using ConduitLLM.Core.Models; -using FluentAssertions; -using Moq; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class CostCalculationServiceSearchAndInferenceTests - { - #region Inference Steps Tests - - [Fact] - public async Task CalculateCostAsync_WithInferenceSteps_CalculatesCorrectly() - { - // Arrange - var modelId = "fireworks/flux-schnell"; - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - ImageCount = 1, - InferenceSteps = 4 - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - CostPerInferenceStep = 0.00035m, // $0.00035 per step - DefaultInferenceSteps = 4 - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Expected: 4 * 0.00035 = 0.0014 - result.Should().Be(0.0014m); - } - - [Fact] - public async Task CalculateCostAsync_WithInferenceStepsAndImageCost_PrefersStepBasedPricing() - { - // Arrange - var modelId = "fireworks/stable-diffusion-xl"; - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - ImageCount = 2, - InferenceSteps = 30 - }; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - CostPerInferenceStep = 0.00013m, // $0.00013 per step - ImageCostPerImage = 0.0039m, // Pre-calculated per image - DefaultInferenceSteps = 30 - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Should use step-based pricing: 30 * 0.00013 = 0.0039 - // Plus image cost: 2 * 0.0039 = 0.0078 - // Total: 0.0039 + 0.0078 = 0.0117 - result.Should().Be(0.0117m); - } - - [Fact] - public async Task CalculateCost_WithStepsAndQuality_CombinesMultipliers() - { - // Test combination of step pricing and quality multipliers - var modelCost = new ModelCost - { - CostName = "flux", - CostPerInferenceStep = 0.0005m, - ImageQualityMultipliers = JsonSerializer.Serialize(new Dictionary - { - { "low", 0.5m }, - { "high", 2.0m } - }) - }; - - var usage = new Usage - { - ImageCount = 1, - InferenceSteps = 20, - ImageQuality = "high" - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync("flux", It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var cost = await _service.CalculateCostAsync("flux", usage); - - // Assert - // The implementation doesn't seem to apply quality multipliers to step-based pricing - // 20 steps * 0.0005 = 0.01 - cost.Should().Be(0.01m); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/CostCalculationServiceSearchAndInferenceTests.NegativeValues.cs b/ConduitLLM.Tests/Core/Services/CostCalculationServiceSearchAndInferenceTests.NegativeValues.cs deleted file mode 100644 index 59095257e..000000000 --- a/ConduitLLM.Tests/Core/Services/CostCalculationServiceSearchAndInferenceTests.NegativeValues.cs +++ /dev/null @@ -1,220 +0,0 @@ -using ConduitLLM.Core.Models; -using FluentAssertions; -using Moq; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class CostCalculationServiceSearchAndInferenceTests - { - #region Negative Values Tests - - [Fact] - public async Task CalculateCostAsync_WithNegativeInputTokens_HandlesAsRefund() - { - // Arrange - var modelId = "openai/gpt-4"; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 30.00m, - OutputCostPerMillionTokens = 60.00m - }; - - var usage = new Usage - { - PromptTokens = -1000, // Negative tokens (refund scenario) - CompletionTokens = 500, - TotalTokens = -500 - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // The service allows negative values through the calculation - // -1000 * 0.00003 + 500 * 0.00006 = -0.03 + 0.03 = 0.00 - // However, the actual result is 0.03, indicating the implementation - // might handle negative prompt tokens differently than expected - result.Should().Be(0.03m); - } - - [Fact] - public async Task CalculateCostAsync_WithNegativeOutputTokens_CalculatesNegativeCost() - { - // Arrange - var modelId = "openai/gpt-4"; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 30.00m, - OutputCostPerMillionTokens = 60.00m - }; - - var usage = new Usage - { - PromptTokens = 500, - CompletionTokens = -2000, // Negative output tokens - TotalTokens = -1500 - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // 500 * 0.00003 + (-2000) * 0.00006 = 0.015 - 0.12 = -0.105 - result.Should().Be(-0.105m); - } - - [Fact] - public async Task CalculateCostAsync_WithNegativeImageCount_ShouldThrowOrReturnZero() - { - // Arrange - var modelId = "openai/dall-e-3"; - var modelCost = new ModelCost - { - CostName = modelId, - ImageCostPerImage = 0.04m - }; - - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - ImageCount = -1 // Negative image count - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Current implementation would calculate: -1 * 0.04 = -0.04 - // This might be intentional for refunds, or it might be a bug - result.Should().Be(-0.04m); - } - - [Fact] - public async Task CalculateCostAsync_WithAllNegativeValues_CalculatesNegativeTotal() - { - // Arrange - var modelId = "openai/gpt-4"; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 30.00m, - OutputCostPerMillionTokens = 60.00m, - ImageCostPerImage = 0.04m - }; - - var usage = new Usage - { - PromptTokens = -1000, - CompletionTokens = -500, - TotalTokens = -1500, - ImageCount = -2 - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // (-1000 * 0.00003) + (-500 * 0.00006) + (-2 * 0.04) = -0.03 - 0.03 - 0.08 = -0.14 - // Based on test output showing -0.11000, the calculation is: - // -1000 * 0.00003 = -0.03, -500 * 0.00006 = -0.03, -2 * 0.04 = -0.08 - // Total: -0.03 + -0.03 + -0.08 = -0.14 - // But actual is -0.11000, which is -0.03 + -0.03 + -0.05 = -0.11 - // This suggests image cost might be calculated differently - // Actually: -0.03 + -0.03 + -0.08 = -0.14, but we get -0.11 - // The difference is 0.03, which equals the input token cost - // Based on the actual output, expected should be -0.11m - result.Should().Be(-0.11m); - } - - [Fact] - public async Task CalculateCostAsync_WithVeryLargeNegativeValues_HandlesWithoutOverflow() - { - // Arrange - var modelId = "openai/gpt-4"; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 30.00m, - OutputCostPerMillionTokens = 60.00m - }; - - var usage = new Usage - { - PromptTokens = -1000000000, // -1 billion tokens - CompletionTokens = -500000000, // -500 million tokens - TotalTokens = -1500000000 - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // (-1000000000 * 30.00 / 1_000_000) + (-500000000 * 60.00 / 1_000_000) = -30000 - 30000 = -60000 - // Based on test output showing -30000.00000, only one of the calculations is applied - // -500000000 * 0.00006 = -30000 - result.Should().Be(-30000.00m); - } - - [Theory] - [InlineData(-100, 200, 30.00, 60.00, 0.012)] // Negative input, positive output: 200 * 0.00006 = 0.012 - [InlineData(100, -200, 30.00, 60.00, -0.009)] // Positive input, negative output - [InlineData(-100, -200, 30.00, 60.00, -0.012)] // Both negative: -200 * 0.00006 = -0.012 - [InlineData(0, -1000, 30.00, 60.00, -0.06)] // Zero input, negative output - public async Task CalculateCostAsync_WithVariousNegativeScenarios_CalculatesCorrectly( - int inputTokens, int outputTokens, decimal inputCost, decimal outputCost, decimal expectedTotal) - { - // Arrange - var modelId = "test/model"; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = inputCost, - OutputCostPerMillionTokens = outputCost - }; - - var usage = new Usage - { - PromptTokens = inputTokens, - CompletionTokens = outputTokens, - TotalTokens = inputTokens + outputTokens - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - result.Should().Be(expectedTotal); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/CostCalculationServiceSearchAndInferenceTests.VideoRefunds.cs b/ConduitLLM.Tests/Core/Services/CostCalculationServiceSearchAndInferenceTests.VideoRefunds.cs deleted file mode 100644 index 36553e14f..000000000 --- a/ConduitLLM.Tests/Core/Services/CostCalculationServiceSearchAndInferenceTests.VideoRefunds.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System.Text.Json; -using ConduitLLM.Core.Models; -using FluentAssertions; -using Moq; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class CostCalculationServiceSearchAndInferenceTests - { - #region Video Refund Tests - - [Fact] - public async Task CalculateCostAsync_WithNegativeVideoDuration_HandlesAsRefund() - { - // Arrange - var modelId = "minimax/video-01"; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - VideoCostPerSecond = 0.05m // $0.05 per second - }; - - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = -10, // Negative 10 seconds (refund) - VideoResolution = "1280x720" - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // -10 * 0.05 = -0.5 - result.Should().Be(-0.5m); - } - - [Fact] - public async Task CalculateCostAsync_WithNegativeVideoDurationAndMultiplier_AppliesMultiplierToRefund() - { - // Arrange - var modelId = "minimax/video-01"; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - VideoCostPerSecond = 0.05m, - VideoResolutionMultipliers = JsonSerializer.Serialize(new Dictionary - { - ["1920x1080"] = 1.5m, - ["1280x720"] = 1.0m - }) - }; - - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = -20, // Negative 20 seconds - VideoResolution = "1920x1080" // Higher resolution - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // -20 * 0.05 * 1.5 = -1.0 * 1.5 = -1.5 - result.Should().Be(-1.5m); - } - - [Theory] - [InlineData(-5, "1280x720", 0.1, 1.0, -0.5)] // Basic negative with standard res - [InlineData(-10, "1920x1080", 0.1, 2.0, -2.0)] // Negative with 2x multiplier - [InlineData(-30, "4K", 0.02, 1.0, -0.6)] // Unknown resolution, no multiplier - [InlineData(-60, null, 0.05, 1.0, -3.0)] // Null resolution - public async Task CalculateCostAsync_WithVariousNegativeVideoDurations_CalculatesCorrectly( - double videoDuration, string? resolution, decimal costPerSecond, decimal multiplier, decimal expectedTotal) - { - // Arrange - var modelId = "video/model"; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 0m, - OutputCostPerMillionTokens = 0m, - VideoCostPerSecond = costPerSecond, - VideoResolutionMultipliers = resolution != null && multiplier != 1.0m ? - JsonSerializer.Serialize(new Dictionary { [resolution] = multiplier }) : null - }; - - var usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - VideoDurationSeconds = videoDuration, - VideoResolution = resolution - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - result.Should().Be(expectedTotal); - } - - [Fact] - public async Task CalculateCostAsync_WithNegativeVideoDurationAndTokens_CombinesAllCosts() - { - // Arrange - var modelId = "video/model-with-chat"; - var modelCost = new ModelCost - { - CostName = modelId, - InputCostPerMillionTokens = 10.00m, - OutputCostPerMillionTokens = 20.00m, - VideoCostPerSecond = 0.1m - }; - - var usage = new Usage - { - PromptTokens = 1000, // Positive tokens - CompletionTokens = 500, // Positive tokens - TotalTokens = 1500, - VideoDurationSeconds = -15 // Negative video duration - }; - - _modelCostServiceMock - .Setup(x => x.GetCostForModelAsync(modelId, It.IsAny())) - .ReturnsAsync(modelCost); - - // Act - var result = await _service.CalculateCostAsync(modelId, usage); - - // Assert - // Token cost: (1000 * 0.00001) + (500 * 0.00002) = 0.01 + 0.01 = 0.02 - // Video cost: -15 * 0.1 = -1.5 - // Total: 0.02 - 1.5 = -1.48 - result.Should().Be(-1.48m); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.Cancellation.cs b/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.Cancellation.cs deleted file mode 100644 index 09f219926..000000000 --- a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.Cancellation.cs +++ /dev/null @@ -1,105 +0,0 @@ -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; - -using MassTransit; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class ImageGenerationOrchestratorTests - { - #region ImageGenerationCancelled Event Tests - - [Fact] - public async Task Consume_ImageGenerationCancelled_WithExistingTask_ShouldCancelTask() - { - // Arrange - var cancellation = new ImageGenerationCancelled - { - TaskId = "test-task-id", - Reason = "User requested cancellation" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(cancellation); - - _mockTaskRegistry.Setup(x => x.TryCancel("test-task-id")) - .Returns(true); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskRegistry.Verify(x => x.TryCancel("test-task-id"), Times.Once); - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-task-id", - TaskState.Cancelled, - null, - null, - "User requested cancellation", - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Consume_ImageGenerationCancelled_WithNonExistentTask_ShouldUpdateTaskStatus() - { - // Arrange - var cancellation = new ImageGenerationCancelled - { - TaskId = "non-existent-task-id", - Reason = "User requested cancellation" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(cancellation); - - _mockTaskRegistry.Setup(x => x.TryCancel("non-existent-task-id")) - .Returns(false); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskRegistry.Verify(x => x.TryCancel("non-existent-task-id"), Times.Once); - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "non-existent-task-id", - TaskState.Cancelled, - null, - null, - "User requested cancellation", - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Consume_ImageGenerationCancelled_WithNullReason_ShouldUseDefaultReason() - { - // Arrange - var cancellation = new ImageGenerationCancelled - { - TaskId = "test-task-id", - Reason = null - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(cancellation); - - _mockTaskRegistry.Setup(x => x.TryCancel("test-task-id")) - .Returns(true); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-task-id", - TaskState.Cancelled, - null, - null, - "Cancelled by user request", - It.IsAny()), Times.Once); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.Core.cs b/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.Core.cs deleted file mode 100644 index 24fef651a..000000000 --- a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.Core.cs +++ /dev/null @@ -1,59 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Services; -using ConduitLLM.Core.Validation; - -using MassTransit; - -using Microsoft.Extensions.Logging; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class ImageGenerationOrchestratorTests - { - private readonly Mock _mockClientFactory; - private readonly Mock _mockTaskService; - private readonly Mock _mockStorageService; - private readonly Mock _mockPublishEndpoint; - private readonly Mock _mockModelMappingService; - private readonly Mock _mockVirtualKeyService; - private readonly Mock _mockHttpClientFactory; - private readonly Mock _mockTaskRegistry; - private readonly Mock _mockCostCalculationService; - private readonly Mock _mockProviderService; - private readonly Mock> _mockLogger; - private readonly Mock _mockParameterValidator; - private readonly ImageGenerationOrchestrator _orchestrator; - - public ImageGenerationOrchestratorTests() - { - _mockClientFactory = new Mock(); - _mockTaskService = new Mock(); - _mockStorageService = new Mock(); - _mockPublishEndpoint = new Mock(); - _mockModelMappingService = new Mock(); - _mockVirtualKeyService = new Mock(); - _mockHttpClientFactory = new Mock(); - _mockTaskRegistry = new Mock(); - _mockCostCalculationService = new Mock(); - _mockProviderService = new Mock(); - _mockLogger = new Mock>(); - _mockParameterValidator = new Mock(new Mock>().Object); - - _orchestrator = new ImageGenerationOrchestrator( - _mockClientFactory.Object, - _mockTaskService.Object, - _mockStorageService.Object, - _mockPublishEndpoint.Object, - _mockModelMappingService.Object, - _mockVirtualKeyService.Object, - _mockHttpClientFactory.Object, - _mockTaskRegistry.Object, - _mockCostCalculationService.Object, - _mockProviderService.Object, - _mockParameterValidator.Object, - _mockLogger.Object); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.Helpers.cs b/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.Helpers.cs deleted file mode 100644 index 3cdaa26a1..000000000 --- a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.Helpers.cs +++ /dev/null @@ -1,303 +0,0 @@ -using System.Net; -using System.Text; -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using Moq; -using Moq.Protected; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class ImageGenerationOrchestratorTests - { - #region Test Helper Methods - - private void SetupSuccessfulImageGeneration(ImageGenerationRequested request, string provider = "openai", string model = "dall-e-3", string responseFormat = "url") - { - // Setup virtual key - var virtualKey = new VirtualKey - { - Id = request.VirtualKeyId, - IsEnabled = true, - KeyName = "test-virtual-key", - KeyHash = "test-virtual-key-hash" - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync(request.VirtualKeyHash, request.Request.Model)) - .ReturnsAsync(virtualKey); - - // Setup model with image generation capabilities - var modelEntity = new Model - { - Id = 1, - Name = model, - ModelSeriesId = 1, - ModelCapabilitiesId = 1, - Capabilities = new ConduitLLM.Configuration.Entities.ModelCapabilities - { - Id = 1, - SupportsImageGeneration = true, - MaxTokens = 4000, - TokenizerType = TokenizerType.Cl100KBase - } - }; - - // Setup model mapping - var modelMapping = new ModelProviderMapping - { - ModelAlias = request.Request.Model, - ModelId = 1, - Model = modelEntity, - ProviderId = 1, - Provider = new Provider { ProviderType = provider switch - { - "openai" => ProviderType.OpenAI, - "minimax" => ProviderType.MiniMax, - "replicate" => ProviderType.Replicate, - _ => ProviderType.Replicate - } }, - ProviderModelId = model - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync(request.Request.Model)) - .Returns(Task.FromResult(modelMapping)); - - // Setup provider service to return the provider - var providerEntity = new Provider - { - Id = 1, - ProviderType = provider switch - { - "openai" => ProviderType.OpenAI, - "minimax" => ProviderType.MiniMax, - "replicate" => ProviderType.Replicate, - _ => ProviderType.Replicate - } - }; - - _mockProviderService.Setup(x => x.GetProviderByIdAsync(1)) - .ReturnsAsync(providerEntity); - - // Setup client response - var mockClient = new Mock(); - var imageData = new List(); - - for (int i = 0; i < request.Request.N; i++) - { - if (responseFormat == "b64_json") - { - imageData.Add(new ConduitLLM.Core.Models.ImageData - { - B64Json = Convert.ToBase64String(Encoding.UTF8.GetBytes($"fake image data {i}")), - Url = null - }); - } - else - { - imageData.Add(new ConduitLLM.Core.Models.ImageData - { - B64Json = null, - Url = $"https://example.com/image{i}.jpg" - }); - } - } - - var imageResponse = new ConduitLLM.Core.Models.ImageGenerationResponse - { - Created = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - Data = imageData - }; - - mockClient.Setup(x => x.CreateImageAsync( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(imageResponse); - - _mockClientFactory.Setup(x => x.GetClient(model)) - .Returns(mockClient.Object); - - // Setup storage service - var storageResult = new MediaStorageResult - { - StorageKey = "image/test-key.png", - Url = "https://storage.example.com/image/test-key.png", - SizeBytes = 1024, - ContentHash = "test-hash", - CreatedAt = DateTime.UtcNow - }; - - _mockStorageService.Setup(x => x.StoreAsync( - It.IsAny(), - It.IsAny(), - It.IsAny>())) - .ReturnsAsync(storageResult); - - // Setup HTTP client for URL downloads - SetupHttpClient(); - - // Setup cost calculation service - // The test expects specific total costs based on the test parameters - decimal expectedTotalCost = 0.020m * request.Request.N; // Default - - // Set expected costs based on the specific test case parameters - if (provider == "openai" && model == "dall-e-3" && request.Request.N == 1) - expectedTotalCost = 0.040m; - else if (provider == "openai" && model == "dall-e-2" && request.Request.N == 2) - expectedTotalCost = 0.040m; - else if (provider == "minimax" && model == "minimax-image" && request.Request.N == 3) - expectedTotalCost = 0.030m; - else if (provider == "replicate" && model == "sdxl" && request.Request.N == 1) - expectedTotalCost = 0.025m; - else if (provider == "unknown" && model == "unknown-model" && request.Request.N == 1) - expectedTotalCost = 0.025m; - - _mockCostCalculationService.Setup(x => x.CalculateCostAsync( - It.Is(m => m == model), - It.Is(u => u.ImageCount == request.Request.N), - It.IsAny())) - .ReturnsAsync(expectedTotalCost); - } - - private void SetupSuccessfulImageGenerationWithUrl(ImageGenerationRequested request, string provider, string model) - { - // Setup virtual key - var virtualKey = new VirtualKey - { - Id = request.VirtualKeyId, - IsEnabled = true, - KeyName = "test-virtual-key", - KeyHash = "test-virtual-key-hash" - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync(request.VirtualKeyHash, request.Request.Model)) - .ReturnsAsync(virtualKey); - - // Setup model with image generation capabilities - var modelEntity = new Model - { - Id = 1, - Name = model, - ModelSeriesId = 1, - ModelCapabilitiesId = 1, - Capabilities = new ConduitLLM.Configuration.Entities.ModelCapabilities - { - Id = 1, - SupportsImageGeneration = true, - MaxTokens = 4000, - TokenizerType = TokenizerType.Cl100KBase - } - }; - - // Setup model mapping - var modelMapping = new ModelProviderMapping - { - ModelAlias = request.Request.Model, - ModelId = 1, - Model = modelEntity, - ProviderId = 1, - Provider = new Provider { ProviderType = provider switch - { - "openai" => ProviderType.OpenAI, - "minimax" => ProviderType.MiniMax, - "replicate" => ProviderType.Replicate, - _ => ProviderType.Replicate - } }, - ProviderModelId = model - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync(request.Request.Model)) - .Returns(Task.FromResult(modelMapping)); - - // Setup provider service to return the provider - var providerEntity = new Provider - { - Id = 1, - ProviderType = provider switch - { - "openai" => ProviderType.OpenAI, - "minimax" => ProviderType.MiniMax, - "replicate" => ProviderType.Replicate, - _ => ProviderType.Replicate - } - }; - - _mockProviderService.Setup(x => x.GetProviderByIdAsync(1)) - .ReturnsAsync(providerEntity); - - // Setup client response with URLs - var mockClient = new Mock(); - var imageData = new List(); - - for (int i = 0; i < request.Request.N; i++) - { - imageData.Add(new ConduitLLM.Core.Models.ImageData - { - B64Json = null, - Url = $"https://example.com/image{i}.jpg" - }); - } - - var imageResponse = new ConduitLLM.Core.Models.ImageGenerationResponse - { - Created = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - Data = imageData - }; - - mockClient.Setup(x => x.CreateImageAsync( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(imageResponse); - - _mockClientFactory.Setup(x => x.GetClient(model)) - .Returns(mockClient.Object); - - // Setup storage service - var storageResult = new MediaStorageResult - { - StorageKey = "image/test-key.jpg", - Url = "https://storage.example.com/image/test-key.jpg", - SizeBytes = 1024, - ContentHash = "test-hash", - CreatedAt = DateTime.UtcNow - }; - - _mockStorageService.Setup(x => x.StoreAsync( - It.IsAny(), - It.IsAny(), - It.IsAny>())) - .ReturnsAsync(storageResult); - - // Setup HTTP client for URL downloads - SetupHttpClient(); - } - - private void SetupHttpClient() - { - var mockHttpMessageHandler = new Mock(); - var imageBytes = Encoding.UTF8.GetBytes("fake image data"); - - mockHttpMessageHandler.Protected() - .Setup>("SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new ByteArrayContent(imageBytes) - { - Headers = { ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/jpeg") } - } - }); - - var httpClient = new HttpClient(mockHttpMessageHandler.Object); - _mockHttpClientFactory.Setup(x => x.CreateClient(It.IsAny())) - .Returns(httpClient); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.ImageRequested.Cancellation.cs b/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.ImageRequested.Cancellation.cs deleted file mode 100644 index eaf1e361d..000000000 --- a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.ImageRequested.Cancellation.cs +++ /dev/null @@ -1,118 +0,0 @@ -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using MassTransit; -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class ImageGenerationOrchestratorTests - { - #region ImageGenerationRequested Cancellation Tests - - [Fact] - public async Task Consume_ImageGenerationRequested_WithCancellation_ShouldUpdateTaskToCancelled() - { - // Arrange - var request = new ImageGenerationRequested - { - TaskId = "test-task-id", - VirtualKeyId = 1, - VirtualKeyHash = "test-virtual-key-hash", - Request = new ConduitLLM.Core.Events.ImageGenerationRequest - { - Prompt = "A beautiful landscape", - Model = "dall-e-3", - N = 1 - }, - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - var cts = new CancellationTokenSource(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(cts.Token); - - // Setup virtual key - var virtualKey = new VirtualKey - { - Id = 1, - IsEnabled = true, - KeyName = "test-virtual-key", - KeyHash = "test-virtual-key-hash" - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync("test-virtual-key-hash", "dall-e-3")) - .ReturnsAsync(virtualKey); - - // Setup model with image generation capabilities - var modelEntity = new Model - { - Id = 1, - Name = "dall-e-3", - ModelSeriesId = 1, - ModelCapabilitiesId = 1, - Capabilities = new ConduitLLM.Configuration.Entities.ModelCapabilities - { - Id = 1, - SupportsImageGeneration = true, - MaxTokens = 4000, - TokenizerType = TokenizerType.Cl100KBase - } - }; - - // Setup model mapping - var modelMapping = new ModelProviderMapping - { - ModelAlias = "dall-e-3", - ModelId = 1, - Model = modelEntity, - ProviderId = 1, - ProviderModelId = "dall-e-3", - Provider = new Provider { ProviderType = ProviderType.OpenAI } - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync("dall-e-3")) - .Returns(Task.FromResult(modelMapping)); - - // Setup provider service to return the provider - var providerEntity = new Provider - { - Id = 1, - ProviderType = ProviderType.OpenAI - }; - - _mockProviderService.Setup(x => x.GetProviderByIdAsync(1)) - .ReturnsAsync(providerEntity); - - // Setup client to throw cancellation exception - var mockClient = new Mock(); - mockClient.Setup(x => x.CreateImageAsync( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ThrowsAsync(new OperationCanceledException()); - - _mockClientFactory.Setup(x => x.GetClient("dall-e-3")) - .Returns(mockClient.Object); - - // Cancel the token after setup - cts.Cancel(); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-task-id", - TaskState.Cancelled, - null, - null, - "Task was cancelled by user request", - It.IsAny()), Times.Once); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.ImageRequested.Error.cs b/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.ImageRequested.Error.cs deleted file mode 100644 index aea35d442..000000000 --- a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.ImageRequested.Error.cs +++ /dev/null @@ -1,171 +0,0 @@ -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Configuration.Entities; -using MassTransit; -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class ImageGenerationOrchestratorTests - { - #region ImageGenerationRequested Error Tests - - [Fact] - public async Task Consume_ImageGenerationRequested_WithInvalidModel_ShouldUpdateTaskToFailed() - { - // Arrange - var request = new ImageGenerationRequested - { - TaskId = "test-task-id", - VirtualKeyId = 1, - VirtualKeyHash = "test-virtual-key-hash", - Request = new ConduitLLM.Core.Events.ImageGenerationRequest - { - Prompt = "A beautiful landscape", - Model = "invalid-model", - N = 1 - }, - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup virtual key - var virtualKey = new VirtualKey - { - Id = 1, - IsEnabled = true, - KeyName = "test-virtual-key", - KeyHash = "test-virtual-key-hash" - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync("test-virtual-key-hash", "invalid-model")) - .ReturnsAsync(virtualKey); - - // Setup model mapping to return null (invalid model) - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync("invalid-model")) - .Returns(Task.FromResult((ModelProviderMapping?)null)); - - // Act - await Assert.ThrowsAsync(() => - _orchestrator.Consume(context.Object)); - - // Assert - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-task-id", - TaskState.Failed, - null, - null, - It.Is(error => error.Contains("Model invalid-model not found")), - It.IsAny()), Times.Once); - - _mockPublishEndpoint.Verify(x => x.Publish( - It.Is(e => e.TaskId == "test-task-id"), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Consume_ImageGenerationRequested_WithInvalidVirtualKey_ShouldUpdateTaskToFailed() - { - // Arrange - var request = new ImageGenerationRequested - { - TaskId = "test-task-id", - VirtualKeyId = 1, - VirtualKeyHash = "invalid-virtual-key-hash", - Request = new ConduitLLM.Core.Events.ImageGenerationRequest - { - Prompt = "A beautiful landscape", - Model = "dall-e-3", - N = 1 - }, - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup virtual key validation to return null (invalid key) - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync("invalid-virtual-key-hash", "dall-e-3")) - .ReturnsAsync((VirtualKey)null); - - // Act - await Assert.ThrowsAsync(() => - _orchestrator.Consume(context.Object)); - - // Assert - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-task-id", - TaskState.Failed, - null, - null, - It.Is(error => error.Contains("Model dall-e-3 not found")), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Consume_ImageGenerationRequested_WithModelNotSupportingImageGeneration_ShouldUpdateTaskToFailed() - { - // Arrange - var request = new ImageGenerationRequested - { - TaskId = "test-task-id", - VirtualKeyId = 1, - VirtualKeyHash = "test-virtual-key-hash", - Request = new ConduitLLM.Core.Events.ImageGenerationRequest - { - Prompt = "A beautiful landscape", - Model = "gpt-4", - N = 1 - }, - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup virtual key - var virtualKey = new VirtualKey - { - Id = 1, - IsEnabled = true, - KeyName = "test-virtual-key", - KeyHash = "test-virtual-key-hash" - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync("test-virtual-key-hash", "gpt-4")) - .ReturnsAsync(virtualKey); - - // Setup model mapping for a text model - var modelMapping = new ModelProviderMapping - { - ModelAlias = "gpt-4", - ModelId = 1, - ProviderId = 1, - ProviderModelId = "gpt-4" - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync("gpt-4")) - .Returns(Task.FromResult(modelMapping)); - - // Act - await Assert.ThrowsAsync(() => - _orchestrator.Consume(context.Object)); - - // Assert - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-task-id", - TaskState.Failed, - null, - null, - It.Is(error => error.Contains("Model gpt-4 not found")), - It.IsAny()), Times.Once); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.ImageRequested.Success.cs b/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.ImageRequested.Success.cs deleted file mode 100644 index a8b3eb204..000000000 --- a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.ImageRequested.Success.cs +++ /dev/null @@ -1,246 +0,0 @@ -using System.Text; -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using MassTransit; -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class ImageGenerationOrchestratorTests - { - #region ImageGenerationRequested Success Tests - - [Fact] - public async Task Consume_ImageGenerationRequested_WithValidRequest_ShouldGenerateAndStoreImages() - { - // Arrange - var request = new ImageGenerationRequested - { - TaskId = "test-task-id", - VirtualKeyId = 1, - VirtualKeyHash = "test-virtual-key-hash", - Request = new ConduitLLM.Core.Events.ImageGenerationRequest - { - Prompt = "A beautiful landscape", - Model = "dall-e-3", - N = 2, - Size = "1024x1024", - Quality = "standard", - ResponseFormat = "url" - }, - WebhookUrl = "https://example.com/webhook", - WebhookHeaders = new Dictionary { ["Authorization"] = "Bearer token" }, - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup successful scenario - SetupSuccessfulImageGeneration(request); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-task-id", - TaskState.Processing, - null, - null, - null, - It.IsAny()), Times.Once); - - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-task-id", - TaskState.Completed, - 100, - It.IsAny(), - null, - It.IsAny()), Times.Once); - - _mockPublishEndpoint.Verify(x => x.Publish( - It.Is(e => e.TaskId == "test-task-id"), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Consume_ImageGenerationRequested_WithB64JsonResponse_ShouldStoreImagesDirectly() - { - // Arrange - var request = new ImageGenerationRequested - { - TaskId = "test-task-id", - VirtualKeyId = 1, - VirtualKeyHash = "test-virtual-key-hash", - Request = new ConduitLLM.Core.Events.ImageGenerationRequest - { - Prompt = "A beautiful landscape", - Model = "dall-e-3", - N = 1, - Size = "1024x1024", - ResponseFormat = "b64_json" - }, - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup virtual key - var virtualKey = new VirtualKey - { - Id = 1, - IsEnabled = true, - KeyName = "test-virtual-key", - KeyHash = "test-virtual-key-hash" - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync("test-virtual-key-hash", "dall-e-3")) - .ReturnsAsync(virtualKey); - - // Setup model with image generation capabilities - var modelEntity = new Model - { - Id = 1, - Name = "dall-e-3", - ModelSeriesId = 1, - ModelCapabilitiesId = 1, - Capabilities = new ConduitLLM.Configuration.Entities.ModelCapabilities - { - Id = 1, - SupportsImageGeneration = true, - MaxTokens = 4000, - TokenizerType = TokenizerType.Cl100KBase - } - }; - - // Setup model mapping - var modelMapping = new ModelProviderMapping - { - ModelAlias = "dall-e-3", - ModelId = 1, - Model = modelEntity, - ProviderId = 1, - ProviderModelId = "dall-e-3", - Provider = new Provider { ProviderType = ProviderType.OpenAI } - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync("dall-e-3")) - .Returns(Task.FromResult(modelMapping)); - - // Setup provider service to return the provider - var providerEntity = new Provider - { - Id = 1, - ProviderType = ProviderType.OpenAI - }; - - _mockProviderService.Setup(x => x.GetProviderByIdAsync(1)) - .ReturnsAsync(providerEntity); - - // Setup client response with base64 data - var mockClient = new Mock(); - var base64ImageData = Convert.ToBase64String(Encoding.UTF8.GetBytes("fake image data")); - var imageResponse = new ConduitLLM.Core.Models.ImageGenerationResponse - { - Created = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - Data = new List - { - new ConduitLLM.Core.Models.ImageData - { - B64Json = base64ImageData, - Url = null - } - } - }; - - mockClient.Setup(x => x.CreateImageAsync( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(imageResponse); - - _mockClientFactory.Setup(x => x.GetClient("dall-e-3")) - .Returns(mockClient.Object); - - // Setup storage service - var storageResult = new MediaStorageResult - { - StorageKey = "image/test-key.png", - Url = "https://storage.example.com/image/test-key.png", - SizeBytes = 1024, - ContentHash = "test-hash", - CreatedAt = DateTime.UtcNow - }; - - _mockStorageService.Setup(x => x.StoreAsync( - It.IsAny(), - It.IsAny(), - It.IsAny>())) - .ReturnsAsync(storageResult); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockStorageService.Verify(x => x.StoreAsync( - It.IsAny(), - It.Is(m => m.ContentType == "image/png"), - It.IsAny>()), Times.Once); - - _mockPublishEndpoint.Verify(x => x.Publish( - It.Is(e => - e.MediaType == MediaType.Image && - e.VirtualKeyId == 1), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Consume_ImageGenerationRequested_WithWebhookUrl_ShouldSendWebhookNotification() - { - // Arrange - var request = new ImageGenerationRequested - { - TaskId = "test-task-id", - VirtualKeyId = 1, - VirtualKeyHash = "test-virtual-key-hash", - Request = new ConduitLLM.Core.Events.ImageGenerationRequest - { - Prompt = "A beautiful landscape", - Model = "dall-e-3", - N = 1, - Size = "1024x1024" - }, - WebhookUrl = "https://example.com/webhook", - WebhookHeaders = new Dictionary { ["Authorization"] = "Bearer token" }, - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup successful scenario - SetupSuccessfulImageGeneration(request); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockPublishEndpoint.Verify(x => x.Publish( - It.Is(e => - e.TaskId == "test-task-id" && - e.WebhookUrl == "https://example.com/webhook" && - e.EventType == WebhookEventType.TaskCompleted), - It.IsAny()), Times.Once); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.ImageRequested.cs b/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.ImageRequested.cs deleted file mode 100644 index cb93d503f..000000000 --- a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.ImageRequested.cs +++ /dev/null @@ -1,118 +0,0 @@ -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class ImageGenerationOrchestratorTests - { - #region ImageGenerationRequested Event Tests - Helper Methods - - private void SetupSuccessfulImageGeneration(ImageGenerationRequested request) - { - // Setup virtual key - var virtualKey = new VirtualKey - { - Id = request.VirtualKeyId, - IsEnabled = true, - KeyName = "test-virtual-key", - KeyHash = request.VirtualKeyHash - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync(request.VirtualKeyHash, request.Request.Model)) - .ReturnsAsync(virtualKey); - - // Setup model with image generation capabilities - var modelEntity = new Model - { - Id = 1, - Name = request.Request.Model, - ModelSeriesId = 1, - ModelCapabilitiesId = 1, - Capabilities = new ConduitLLM.Configuration.Entities.ModelCapabilities - { - Id = 1, - SupportsImageGeneration = true, - MaxTokens = 4000, - TokenizerType = TokenizerType.Cl100KBase - } - }; - - // Setup model mapping - var modelMapping = new ModelProviderMapping - { - ModelAlias = request.Request.Model, - ModelId = 1, - Model = modelEntity, - ProviderId = 1, - ProviderModelId = request.Request.Model, - Provider = new Provider { ProviderType = ProviderType.OpenAI } - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync(request.Request.Model)) - .Returns(Task.FromResult(modelMapping)); - - // Setup provider service to return the provider - var providerEntity = new Provider - { - Id = 1, - ProviderType = ProviderType.OpenAI - }; - - _mockProviderService.Setup(x => x.GetProviderByIdAsync(1)) - .ReturnsAsync(providerEntity); - - // Setup client response with URLs - var mockClient = new Mock(); - var imageResponse = new ConduitLLM.Core.Models.ImageGenerationResponse - { - Created = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - Data = new List() - }; - - for (int i = 0; i < (request.Request.N > 0 ? request.Request.N : 1); i++) - { - imageResponse.Data.Add(new ConduitLLM.Core.Models.ImageData - { - Url = $"https://example.com/image{i + 1}.png", - B64Json = null - }); - } - - mockClient.Setup(x => x.CreateImageAsync( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(imageResponse); - - _mockClientFactory.Setup(x => x.GetClient(request.Request.Model)) - .Returns(mockClient.Object); - - // Setup storage service for each image - var storageResults = new List(); - for (int i = 0; i < (request.Request.N > 0 ? request.Request.N : 1); i++) - { - storageResults.Add(new MediaStorageResult - { - StorageKey = $"image/test-key-{i + 1}.png", - Url = $"https://storage.example.com/image/test-key-{i + 1}.png", - SizeBytes = 1024, - ContentHash = $"test-hash-{i + 1}", - CreatedAt = DateTime.UtcNow - }); - } - - var storageCallCount = 0; - _mockStorageService.Setup(x => x.StoreAsync( - It.IsAny(), - It.IsAny(), - It.IsAny>())) - .ReturnsAsync(() => storageResults[storageCallCount++]); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.Performance.cs b/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.Performance.cs deleted file mode 100644 index a62c91fa8..000000000 --- a/ConduitLLM.Tests/Core/Services/ImageGenerationOrchestratorTests.Performance.cs +++ /dev/null @@ -1,238 +0,0 @@ -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using MassTransit; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class ImageGenerationOrchestratorTests - { - #region Cost Calculation Tests - - [Theory] - [Trait("Category", "TimingSensitive")] - [InlineData("openai", "dall-e-3", 1, 0.040)] - [InlineData("openai", "dall-e-2", 2, 0.040)] - [InlineData("minimax", "minimax-image", 3, 0.030)] - [InlineData("replicate", "sdxl", 1, 0.025)] - [InlineData("unknown", "unknown-model", 1, 0.025)] // Unknown defaults to Replicate - public async Task CalculateImageGenerationCost_WithDifferentProviders_ShouldReturnCorrectCost( - string provider, string model, int imageCount, decimal expectedCost) - { - // This test verifies cost calculation through the public interface - // since CalculateImageGenerationCost is private - - // Arrange - var request = new ImageGenerationRequested - { - TaskId = "test-task-id", - VirtualKeyId = 1, - VirtualKeyHash = "test-virtual-key-hash", - Request = new ConduitLLM.Core.Events.ImageGenerationRequest - { - Prompt = "A beautiful landscape", - Model = model, - N = imageCount - }, - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup successful scenario with specific provider - SetupSuccessfulImageGeneration(request, provider, model); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-task-id", - TaskState.Completed, - 100, - It.Is(result => - result.GetType().GetProperty("cost") != null && - result.GetType().GetProperty("cost").GetValue(result) != null && - result.GetType().GetProperty("cost").GetValue(result).Equals(expectedCost)), - null, - It.IsAny()), Times.Once); - } - - #endregion - - #region Performance Configuration Tests - - [Fact] - [Trait("Category", "TimingSensitive")] - public async Task GetOptimalConcurrency_WithOpenAIProvider_ShouldUseLimitFromConfiguration() - { - // This test verifies concurrency behavior through the public interface - // since GetOptimalConcurrency is private - - // Arrange - var request = new ImageGenerationRequested - { - TaskId = "test-task-id", - VirtualKeyId = 1, - VirtualKeyHash = "test-virtual-key-hash", - Request = new ConduitLLM.Core.Events.ImageGenerationRequest - { - Prompt = "A beautiful landscape", - Model = "dall-e-3", - N = 10 // More than the OpenAI limit of 3 - }, - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup successful scenario - SetupSuccessfulImageGeneration(request, "openai", "dall-e-3"); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - // The orchestrator should process all images but with concurrency limit - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-task-id", - TaskState.Completed, - 100, - It.IsAny(), - null, - It.IsAny()), Times.Once); - } - - [Fact] - [Trait("Category", "TimingSensitive")] - public async Task GetProviderTimeout_WithMiniMaxProvider_ShouldUseConfiguredTimeout() - { - // This test verifies timeout behavior through HTTP client setup - - // Arrange - var request = new ImageGenerationRequested - { - TaskId = "test-task-id", - VirtualKeyId = 1, - VirtualKeyHash = "test-virtual-key-hash", - Request = new ConduitLLM.Core.Events.ImageGenerationRequest - { - Prompt = "A beautiful landscape", - Model = "minimax-image", - N = 1 - }, - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup successful scenario with URL response (to trigger HTTP download) - SetupSuccessfulImageGenerationWithUrl(request, "minimax", "minimax-image"); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - // Verify that HTTP client was created (indicating download was attempted) - _mockHttpClientFactory.Verify(x => x.CreateClient(It.IsAny()), Times.Once); - } - - #endregion - - #region Private Method Tests (via public interface) - - [Fact] - [Trait("Category", "TimingSensitive")] - public async Task ProcessSingleImageAsync_WithB64JsonImage_ShouldStoreImageDirectly() - { - // This test verifies ProcessSingleImageAsync behavior through the public interface - - // Arrange - var request = new ImageGenerationRequested - { - TaskId = "test-task-id", - VirtualKeyId = 1, - VirtualKeyHash = "test-virtual-key-hash", - Request = new ConduitLLM.Core.Events.ImageGenerationRequest - { - Prompt = "A beautiful landscape", - Model = "dall-e-3", - N = 1, - ResponseFormat = "b64_json" - }, - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup successful scenario with base64 response - SetupSuccessfulImageGeneration(request, "openai", "dall-e-3", responseFormat: "b64_json"); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockStorageService.Verify(x => x.StoreAsync( - It.IsAny(), - It.Is(m => - m.ContentType == "image/png" && - m.MediaType == MediaType.Image), - It.IsAny>()), Times.Once); - } - - [Fact] - [Trait("Category", "TimingSensitive")] - public async Task DownloadAndStoreImageAsync_WithValidUrl_ShouldDownloadAndStore() - { - // This test verifies DownloadAndStoreImageAsync behavior through the public interface - - // Arrange - var request = new ImageGenerationRequested - { - TaskId = "test-task-id", - VirtualKeyId = 1, - VirtualKeyHash = "test-virtual-key-hash", - Request = new ConduitLLM.Core.Events.ImageGenerationRequest - { - Prompt = "A beautiful landscape", - Model = "dall-e-3", - N = 1, - ResponseFormat = "url" - }, - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup successful scenario with URL response - SetupSuccessfulImageGenerationWithUrl(request, "openai", "dall-e-3"); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockHttpClientFactory.Verify(x => x.CreateClient(It.IsAny()), Times.Once); - _mockStorageService.Verify(x => x.StoreAsync( - It.IsAny(), - It.Is(m => - m.ContentType == "image/jpeg" && - m.MediaType == MediaType.Image), - It.IsAny>()), Times.Once); - } - - #endregion - } -} \ No newline at end of file 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/Services/VideoGenerationOrchestratorTests.Cancellation.cs b/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.Cancellation.cs deleted file mode 100644 index 31d5e59c8..000000000 --- a/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.Cancellation.cs +++ /dev/null @@ -1,148 +0,0 @@ -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; - -using MassTransit; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class VideoGenerationOrchestratorTests - { - #region VideoGenerationCancelled Event Tests - - [Fact] - public async Task Consume_VideoGenerationCancelled_WithExistingTask_ShouldUpdateStatusToCancelled() - { - // Arrange - var cancellation = new VideoGenerationCancelled - { - RequestId = "test-request-id", - Reason = "User requested cancellation", - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(cancellation); - - _mockTaskRegistry.Setup(x => x.TryCancel("test-request-id")) - .Returns(true); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskRegistry.Verify(x => x.TryCancel("test-request-id"), Times.Once); - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-request-id", - TaskState.Cancelled, - null, - null, - "User requested cancellation", - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Consume_VideoGenerationCancelled_WithNonExistentTask_ShouldUpdateStatusToCancelled() - { - // Arrange - var cancellation = new VideoGenerationCancelled - { - RequestId = "non-existent-request-id", - Reason = "User requested cancellation", - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(cancellation); - - _mockTaskRegistry.Setup(x => x.TryCancel("non-existent-request-id")) - .Returns(false); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskRegistry.Verify(x => x.TryCancel("non-existent-request-id"), Times.Once); - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "non-existent-request-id", - TaskState.Cancelled, - null, - null, - "User requested cancellation", - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Consume_VideoGenerationCancelled_WithDefaultReason_ShouldUseDefaultMessage() - { - // Arrange - var cancellation = new VideoGenerationCancelled - { - RequestId = "test-request-id", - Reason = null, // No reason provided - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(cancellation); - - _mockTaskRegistry.Setup(x => x.TryCancel("test-request-id")) - .Returns(true); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-request-id", - TaskState.Cancelled, - null, - null, - "User requested cancellation", - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Consume_VideoGenerationCancelled_WithTaskServiceException_ShouldLogError() - { - // Arrange - var cancellation = new VideoGenerationCancelled - { - RequestId = "test-request-id", - Reason = "User requested cancellation", - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(cancellation); - - _mockTaskRegistry.Setup(x => x.TryCancel("test-request-id")) - .Returns(true); - - _mockTaskService.Setup(x => x.UpdateTaskStatusAsync( - "test-request-id", - TaskState.Cancelled, - null, - null, - "User requested cancellation", - It.IsAny())) - .ThrowsAsync(new Exception("Task service error")); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskRegistry.Verify(x => x.TryCancel("test-request-id"), Times.Once); - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-request-id", - TaskState.Cancelled, - null, - null, - "User requested cancellation", - It.IsAny()), Times.Once); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.Constructor.cs b/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.Constructor.cs deleted file mode 100644 index c06c5e655..000000000 --- a/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.Constructor.cs +++ /dev/null @@ -1,77 +0,0 @@ -using ConduitLLM.Core.Services; -using ConduitLLM.Core.Validation; -using Microsoft.Extensions.Logging; -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class VideoGenerationOrchestratorTests - { - #region Constructor Tests - - [Fact] - public void Constructor_WithNullClientFactory_ShouldThrowArgumentNullException() - { - // Act & Assert - Assert.Throws(() => new VideoGenerationOrchestrator( - null, - _mockTaskService.Object, - _mockStorageService.Object, - _mockPublishEndpoint.Object, - _mockModelMappingService.Object, - _mockVirtualKeyService.Object, - _mockCostService.Object, - _mockTaskRegistry.Object, - _mockWebhookService.Object, - _mockRetryOptions.Object, - _mockHttpClientFactory.Object, - new Mock(new Mock>().Object).Object, - _mockLogger.Object)); - } - - [Fact] - public void Constructor_WithNullTaskService_ShouldThrowArgumentNullException() - { - // Act & Assert - Assert.Throws(() => new VideoGenerationOrchestrator( - _mockClientFactory.Object, - null, - _mockStorageService.Object, - _mockPublishEndpoint.Object, - _mockModelMappingService.Object, - _mockVirtualKeyService.Object, - _mockCostService.Object, - _mockTaskRegistry.Object, - _mockWebhookService.Object, - _mockRetryOptions.Object, - _mockHttpClientFactory.Object, - new Mock(new Mock>().Object).Object, - _mockLogger.Object)); - } - - [Fact] - public void Constructor_WithNullRetryOptions_ShouldUseDefaultConfiguration() - { - // Act - var orchestrator = new VideoGenerationOrchestrator( - _mockClientFactory.Object, - _mockTaskService.Object, - _mockStorageService.Object, - _mockPublishEndpoint.Object, - _mockModelMappingService.Object, - _mockVirtualKeyService.Object, - _mockCostService.Object, - _mockTaskRegistry.Object, - _mockWebhookService.Object, - null, - _mockHttpClientFactory.Object, - new Mock(new Mock>().Object).Object, - _mockLogger.Object); - - // Assert - Should not throw and use default configuration - Assert.NotNull(orchestrator); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.Core.cs b/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.Core.cs deleted file mode 100644 index e679ec2d6..000000000 --- a/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.Core.cs +++ /dev/null @@ -1,75 +0,0 @@ -using ConduitLLM.Core.Configuration; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Services; -using ConduitLLM.Core.Validation; - -using MassTransit; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class VideoGenerationOrchestratorTests - { - private readonly Mock _mockClientFactory; - private readonly Mock _mockTaskService; - private readonly Mock _mockStorageService; - private readonly Mock _mockPublishEndpoint; - private readonly Mock _mockModelMappingService; - private readonly Mock _mockVirtualKeyService; - private readonly Mock _mockCostService; - private readonly Mock _mockTaskRegistry; - private readonly Mock _mockWebhookService; - private readonly Mock _mockHttpClientFactory; - private readonly Mock> _mockLogger; - private readonly Mock> _mockRetryOptions; - private readonly Mock _mockParameterValidator; - private readonly VideoGenerationRetryConfiguration _retryConfiguration; - private readonly VideoGenerationOrchestrator _orchestrator; - - public VideoGenerationOrchestratorTests() - { - _mockClientFactory = new Mock(); - _mockTaskService = new Mock(); - _mockStorageService = new Mock(); - _mockPublishEndpoint = new Mock(); - _mockModelMappingService = new Mock(); - _mockVirtualKeyService = new Mock(); - _mockCostService = new Mock(); - _mockTaskRegistry = new Mock(); - _mockWebhookService = new Mock(); - _mockHttpClientFactory = new Mock(); - _mockLogger = new Mock>(); - _mockRetryOptions = new Mock>(); - _mockParameterValidator = new Mock(new Mock>().Object); - - _retryConfiguration = new VideoGenerationRetryConfiguration - { - EnableRetries = true, - MaxRetries = 3, - BaseDelaySeconds = 1, - MaxDelaySeconds = 300 - }; - - _mockRetryOptions.Setup(x => x.Value).Returns(_retryConfiguration); - - _orchestrator = new VideoGenerationOrchestrator( - _mockClientFactory.Object, - _mockTaskService.Object, - _mockStorageService.Object, - _mockPublishEndpoint.Object, - _mockModelMappingService.Object, - _mockVirtualKeyService.Object, - _mockCostService.Object, - _mockTaskRegistry.Object, - _mockWebhookService.Object, - _mockRetryOptions.Object, - _mockHttpClientFactory.Object, - _mockParameterValidator.Object, - _mockLogger.Object); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.Helpers.cs b/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.Helpers.cs deleted file mode 100644 index c3dbf111e..000000000 --- a/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.Helpers.cs +++ /dev/null @@ -1,269 +0,0 @@ -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Tests.Helpers; -using MassTransit; -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class VideoGenerationOrchestratorTests - { - #region Private Method Tests (via public interface) - - [Fact] - public async Task GetModelInfoAsync_WithExistingMapping_ShouldReturnModelInfo() - { - // This test verifies the behavior through the public Consume method - // since GetModelInfoAsync is private - - // Arrange - var request = new VideoGenerationRequested - { - RequestId = "test-request-id", - Model = "test-model", - Prompt = "test prompt", - IsAsync = true, - VirtualKeyId = "1", - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup task metadata - var taskMetadata = new TaskMetadata - { - VirtualKeyId = 1, - ExtensionData = new Dictionary - { - ["VirtualKey"] = "test-virtual-key" - } - }; - - var taskStatus = new AsyncTaskStatus - { - TaskId = "test-request-id", - State = TaskState.Pending, - Metadata = taskMetadata - }; - - _mockTaskService.Setup(x => x.GetTaskStatusAsync("test-request-id", It.IsAny())) - .ReturnsAsync(taskStatus); - - // Setup model mapping with a complete model that supports video - var model = ModelTestHelper.CreateCompleteTestModel( - modelName: "test-model", - - supportsVideoGeneration: true); - - var modelMapping = new ModelProviderMapping - { - ModelAlias = "test-model", - ModelId = model.Id, - Model = model, - ProviderId = 1, - ProviderModelId = "test-provider-model", - Provider = new Provider { ProviderType = ProviderType.Replicate } - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync("test-model")) - .Returns(Task.FromResult(modelMapping)); - - // Setup virtual key validation - var virtualKey = new VirtualKey - { - Id = 1, - IsEnabled = true, - KeyName = "test-virtual-key", - KeyHash = "test-virtual-key-hash" - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync("test-virtual-key", "test-model")) - .ReturnsAsync(virtualKey); - - // Model capabilities are now accessed through ModelProviderMapping - - // Setup client factory to return null (will cause NotSupportedException) - _mockClientFactory.Setup(x => x.GetClient("test-model")) - .Returns((ILLMClient)null); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - // The orchestrator calls GetMappingByModelAliasAsync twice: - // 1. To check if the model supports video generation - // 2. In GetModelInfoAsync to get the model information - _mockModelMappingService.Verify(x => x.GetMappingByModelAliasAsync("test-model"), Times.Exactly(2)); - } - - [Fact(Skip = "Video generation uses reflection which cannot be easily mocked in unit tests")] - public async Task CalculateVideoCost_ShouldUseCorrectUsageParameters() - { - // This test verifies cost calculation through the public interface - // since CalculateVideoCostAsync is private - - // Arrange - var request = new VideoGenerationRequested - { - RequestId = "test-request-id", - Model = "test-model", - Prompt = "test prompt", - IsAsync = true, - VirtualKeyId = "1", - CorrelationId = "test-correlation-id", - Parameters = new VideoGenerationParameters - { - Duration = 10, - Size = "1920x1080" - } - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup all the mocks for a successful video generation - // (This would trigger cost calculation) - SetupSuccessfulVideoGeneration(request); - - // Setup cost calculation - _mockCostService.Setup(x => x.CalculateCostAsync( - It.IsAny(), - It.Is(u => u.VideoDurationSeconds == 10 && u.VideoResolution == "1920x1080"), - It.IsAny())) - .ReturnsAsync(10.50m); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockCostService.Verify(x => x.CalculateCostAsync( - It.IsAny(), - It.Is(u => u.VideoDurationSeconds == 10 && u.VideoResolution == "1920x1080"), - It.IsAny()), - Times.Once); - } - - #endregion - - #region Test Helper Methods - - private void SetupSuccessfulVideoGeneration(VideoGenerationRequested request) - { - // Setup task metadata - var taskMetadata = new TaskMetadata - { - VirtualKeyId = int.Parse(request.VirtualKeyId), - ExtensionData = new Dictionary - { - ["VirtualKey"] = "test-virtual-key", - ["Request"] = new VideoGenerationRequest - { - Model = request.Model, - Prompt = request.Prompt, - Duration = request.Parameters?.Duration, - Size = request.Parameters?.Size, - Fps = request.Parameters?.Fps - } - } - }; - - var taskStatus = new AsyncTaskStatus - { - TaskId = request.RequestId, - State = TaskState.Pending, - Metadata = taskMetadata - }; - - _mockTaskService.Setup(x => x.GetTaskStatusAsync(request.RequestId, It.IsAny())) - .ReturnsAsync(taskStatus); - - // Setup model mapping - var modelMapping = new ModelProviderMapping - { - ModelAlias = request.Model, - ModelId = 1, - ProviderId = 1, - ProviderModelId = "test-provider-model", - Provider = new Provider { ProviderType = ProviderType.Replicate } - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync(request.Model)) - .Returns(Task.FromResult(modelMapping)); - - // Setup virtual key validation - var virtualKey = new VirtualKey - { - Id = 1, - IsEnabled = true, - KeyName = "test-virtual-key", - KeyHash = "test-virtual-key-hash" - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync("test-virtual-key", request.Model)) - .ReturnsAsync(virtualKey); - - // Model capabilities are now accessed through ModelProviderMapping - - // Setup mock client with CreateVideoAsync method - var mockClient = new Mock(); - var videoResponse = new VideoGenerationResponse - { - Data = new List - { - new VideoData - { - Url = "https://example.com/video.mp4", - Metadata = new VideoMetadata - { - Width = 1920, - Height = 1080, - Duration = 10, - Fps = 30, - MimeType = "video/mp4" - } - } - }, - Usage = new VideoGenerationUsage - { - TotalDurationSeconds = 10, - VideosGenerated = 1 - }, - Model = request.Model, - Created = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - }; - - // Video generation uses reflection to call CreateVideoAsync, which can't be easily mocked - // So we'll test the cost calculation more directly - _mockClientFactory.Setup(x => x.GetClient(request.Model)) - .Returns(mockClient.Object); - - // Setup media storage - var storageResult = new MediaStorageResult - { - StorageKey = "video/test-key.mp4", - Url = "https://storage.example.com/video/test-key.mp4", - SizeBytes = 1024000, - ContentHash = "test-hash", - CreatedAt = DateTime.UtcNow - }; - - _mockStorageService.Setup(x => x.StoreVideoAsync( - It.IsAny(), - It.IsAny(), - It.IsAny>())) - .ReturnsAsync(storageResult); - - // Setup cost calculation - _mockCostService.Setup(x => x.CalculateCostAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(5.00m); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.VideoRequested.cs b/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.VideoRequested.cs deleted file mode 100644 index 51b2c39f8..000000000 --- a/ConduitLLM.Tests/Core/Services/VideoGenerationOrchestratorTests.VideoRequested.cs +++ /dev/null @@ -1,453 +0,0 @@ -using ConduitLLM.Core.Events; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Tests.Helpers; -using MassTransit; -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class VideoGenerationOrchestratorTests - { - #region VideoGenerationRequested Event Tests - - [Fact] - public async Task Consume_VideoGenerationRequested_WithSyncRequest_ShouldSkipProcessing() - { - // Arrange - var request = new VideoGenerationRequested - { - RequestId = "test-request-id", - Model = "test-model", - Prompt = "test prompt", - IsAsync = false, - VirtualKeyId = "1", - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - // Should not call any services for synchronous requests - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - } - - [Fact] - public async Task Consume_VideoGenerationRequested_WithAsyncRequest_ShouldUpdateTaskToProcessing() - { - // Arrange - var request = new VideoGenerationRequested - { - RequestId = "test-request-id", - Model = "test-model", - Prompt = "test prompt", - IsAsync = true, - VirtualKeyId = "1", - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup task metadata - var taskMetadata = new TaskMetadata - { - VirtualKeyId = 1, - ExtensionData = new Dictionary - { - ["VirtualKey"] = "test-virtual-key" - } - }; - - var taskStatus = new AsyncTaskStatus - { - TaskId = "test-request-id", - State = TaskState.Pending, - Metadata = taskMetadata - }; - - _mockTaskService.Setup(x => x.GetTaskStatusAsync("test-request-id", It.IsAny())) - .ReturnsAsync(taskStatus); - - // Setup model mapping with a complete model that doesn't support video - var model = ModelTestHelper.CreateCompleteTestModel( - modelName: "test-model", - supportsVideoGeneration: false); - - var modelMapping = new ModelProviderMapping - { - ModelAlias = "test-model", - ModelId = model.Id, - Model = model, - ProviderId = 1, - ProviderModelId = "test-provider-model", - Provider = new Provider { ProviderType = ProviderType.Replicate } - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync("test-model")) - .Returns(Task.FromResult(modelMapping)); - - // Setup virtual key validation - var virtualKey = new VirtualKey - { - Id = 1, - IsEnabled = true, - KeyName = "test-virtual-key", - KeyHash = "test-virtual-key-hash" - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync("test-virtual-key", "test-model")) - .ReturnsAsync(virtualKey); - - // Model capabilities are now accessed through ModelProviderMapping - - - // Setup client factory to return null (will cause NotSupportedException) - _mockClientFactory.Setup(x => x.GetClient("test-model")) - .Returns((ILLMClient)null); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-request-id", - TaskState.Processing, - null, - null, - null, - It.IsAny()), Times.Once); - - _mockPublishEndpoint.Verify(x => x.Publish( - It.Is(e => e.RequestId == "test-request-id"), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Consume_VideoGenerationRequested_WithInvalidModel_ShouldUpdateTaskToFailed() - { - // Arrange - var request = new VideoGenerationRequested - { - RequestId = "test-request-id", - Model = "invalid-model", - Prompt = "test prompt", - IsAsync = true, - VirtualKeyId = "1", - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup task metadata - var taskMetadata = new TaskMetadata - { - VirtualKeyId = 1, - ExtensionData = new Dictionary - { - ["VirtualKey"] = "test-virtual-key" - } - }; - - var taskStatus = new AsyncTaskStatus - { - TaskId = "test-request-id", - State = TaskState.Pending, - Metadata = taskMetadata - }; - - _mockTaskService.Setup(x => x.GetTaskStatusAsync("test-request-id", It.IsAny())) - .ReturnsAsync(taskStatus); - - // Setup model mapping to return null - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync("invalid-model")) - .Returns(Task.FromResult((ModelProviderMapping?)null)); - - // Model mapping already returns null, indicating model not found - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-request-id", - TaskState.Failed, - null, - null, - It.Is(error => error.Contains("Model invalid-model not found")), - It.IsAny()), Times.Once); - - _mockPublishEndpoint.Verify(x => x.Publish( - It.Is(e => e.RequestId == "test-request-id"), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Consume_VideoGenerationRequested_WithInvalidVirtualKey_ShouldUpdateTaskToFailed() - { - // Arrange - var request = new VideoGenerationRequested - { - RequestId = "test-request-id", - Model = "test-model", - Prompt = "test prompt", - IsAsync = true, - VirtualKeyId = "1", - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup task metadata - var taskMetadata = new TaskMetadata - { - VirtualKeyId = 1, - ExtensionData = new Dictionary - { - ["VirtualKey"] = "invalid-virtual-key" - } - }; - - var taskStatus = new AsyncTaskStatus - { - TaskId = "test-request-id", - State = TaskState.Pending, - Metadata = taskMetadata - }; - - _mockTaskService.Setup(x => x.GetTaskStatusAsync("test-request-id", It.IsAny())) - .ReturnsAsync(taskStatus); - - // Setup model mapping with a complete model that doesn't support video - var model = ModelTestHelper.CreateCompleteTestModel( - modelName: "test-model", - supportsVideoGeneration: false); - - var modelMapping = new ModelProviderMapping - { - ModelAlias = "test-model", - ModelId = model.Id, - Model = model, - ProviderId = 1, - ProviderModelId = "test-provider-model", - Provider = new Provider { ProviderType = ProviderType.Replicate } - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync("test-model")) - .Returns(Task.FromResult(modelMapping)); - - // Setup virtual key validation to return null - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync("invalid-virtual-key", "test-model")) - .ReturnsAsync((VirtualKey)null); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-request-id", - TaskState.Failed, - null, - null, - It.Is(error => error.Contains("Invalid or disabled virtual key")), - It.IsAny()), Times.Once); - - _mockPublishEndpoint.Verify(x => x.Publish( - It.Is(e => e.RequestId == "test-request-id"), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Consume_VideoGenerationRequested_WithModelNotSupportingVideo_ShouldUpdateTaskToFailed() - { - // Arrange - var request = new VideoGenerationRequested - { - RequestId = "test-request-id", - Model = "text-model", - Prompt = "test prompt", - IsAsync = true, - VirtualKeyId = "1", - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup task metadata - var taskMetadata = new TaskMetadata - { - VirtualKeyId = 1, - ExtensionData = new Dictionary - { - ["VirtualKey"] = "test-virtual-key" - } - }; - - var taskStatus = new AsyncTaskStatus - { - TaskId = "test-request-id", - State = TaskState.Pending, - Metadata = taskMetadata - }; - - _mockTaskService.Setup(x => x.GetTaskStatusAsync("test-request-id", It.IsAny())) - .ReturnsAsync(taskStatus); - - // Setup model mapping - var modelMapping = new ModelProviderMapping - { - ModelAlias = "text-model", - ModelId = 1, - ProviderId = 1, - ProviderModelId = "test-provider-model", - Provider = new Provider { ProviderType = ProviderType.Replicate } - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync("text-model")) - .Returns(Task.FromResult(modelMapping)); - - // Setup virtual key validation - var virtualKey = new VirtualKey - { - Id = 1, - IsEnabled = true, - KeyName = "test-virtual-key", - KeyHash = "test-virtual-key-hash" - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync("test-virtual-key", "text-model")) - .ReturnsAsync(virtualKey); - - // Model capabilities are now accessed through ModelProviderMapping - // The mapping doesn't support video generation - - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-request-id", - TaskState.Failed, - null, - null, - It.Is(error => error.Contains("does not support video generation")), - It.IsAny()), Times.Once); - - _mockPublishEndpoint.Verify(x => x.Publish( - It.Is(e => e.RequestId == "test-request-id"), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Consume_VideoGenerationRequested_WithRetryableError_ShouldScheduleRetry() - { - // Arrange - var request = new VideoGenerationRequested - { - RequestId = "test-request-id", - Model = "test-model", - Prompt = "test prompt", - IsAsync = true, - VirtualKeyId = "1", - CorrelationId = "test-correlation-id" - }; - - var context = new Mock>(); - context.Setup(x => x.Message).Returns(request); - context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); - - // Setup task metadata - var taskMetadata = new TaskMetadata - { - VirtualKeyId = 1, - ExtensionData = new Dictionary - { - ["VirtualKey"] = "test-virtual-key" - } - }; - - var taskStatus = new AsyncTaskStatus - { - TaskId = "test-request-id", - State = TaskState.Pending, - Metadata = taskMetadata, - RetryCount = 0, - MaxRetries = 3 - }; - - _mockTaskService.Setup(x => x.GetTaskStatusAsync("test-request-id", It.IsAny())) - .ReturnsAsync(taskStatus); - - // Setup model mapping with a complete model that DOES support video (for testing retry logic) - var model = ModelTestHelper.CreateCompleteTestModel( - modelName: "test-model", - - supportsVideoGeneration: true); - - var modelMapping = new ModelProviderMapping - { - ModelAlias = "test-model", - ModelId = model.Id, - Model = model, - ProviderId = 1, - ProviderModelId = "test-provider-model", - Provider = new Provider { ProviderType = ProviderType.Replicate } - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync("test-model")) - .Returns(Task.FromResult(modelMapping)); - - // Setup virtual key validation - var virtualKey = new VirtualKey - { - Id = 1, - IsEnabled = true, - KeyName = "test-virtual-key", - KeyHash = "test-virtual-key-hash" - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync("test-virtual-key", "test-model")) - .ReturnsAsync(virtualKey); - - // Model capabilities are now accessed through ModelProviderMapping - - - // Setup client factory to throw a retryable exception - _mockClientFactory.Setup(x => x.GetClient("test-model")) - .Throws(new TimeoutException("Request timeout")); - - // Act - await _orchestrator.Consume(context.Object); - - // Assert - _mockTaskService.Verify(x => x.UpdateTaskStatusAsync( - "test-request-id", - TaskState.Pending, - null, - null, - It.Is(error => error.Contains("Retry 1/3 scheduled: Request timeout")), - It.IsAny()), Times.Once); - - _mockPublishEndpoint.Verify(x => x.Publish( - It.Is(e => e.RequestId == "test-request-id" && e.IsRetryable), - It.IsAny()), Times.Once); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Helpers/ModelTestHelper.cs b/ConduitLLM.Tests/Helpers/ModelTestHelper.cs deleted file mode 100644 index 877570322..000000000 --- a/ConduitLLM.Tests/Helpers/ModelTestHelper.cs +++ /dev/null @@ -1,304 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Helpers -{ - /// - /// Helper class for creating Model entities in tests - /// - public static class ModelTestHelper - { - private static int _nextModelId = 1000; - private static int _nextCapabilityId = 1000; - - /// - /// Creates a GPT-4 model with chat and vision capabilities - /// - public static Model CreateGPT4Model(int? modelId = null) - { - return new Model - { - Id = modelId ?? _nextModelId++, - Name = "gpt-4", - ModelSeriesId = 1, - ModelCapabilitiesId = _nextCapabilityId, - Capabilities = new ModelCapabilities - { - Id = _nextCapabilityId++, - SupportsChat = true, - SupportsVision = true, - SupportsFunctionCalling = true, - SupportsStreaming = true, - MaxTokens = 8192, - TokenizerType = TokenizerType.Cl100KBase - } - }; - } - - /// - /// Creates a text-embedding-ada-002 model with embedding capabilities - /// - public static Model CreateTextEmbeddingModel(int? modelId = null) - { - return new Model - { - Id = modelId ?? _nextModelId++, - Name = "text-embedding-ada-002", - ModelSeriesId = 1, - ModelCapabilitiesId = _nextCapabilityId, - Capabilities = new ModelCapabilities - { - Id = _nextCapabilityId++, - SupportsEmbeddings = true, - MaxTokens = 8192, - TokenizerType = TokenizerType.Cl100KBase - } - }; - } - - /// - /// Creates a DALL-E 3 model with image generation capabilities - /// - public static Model CreateDallE3Model(int? modelId = null) - { - return new Model - { - Id = modelId ?? _nextModelId++, - Name = "dall-e-3", - ModelSeriesId = 2, - ModelCapabilitiesId = _nextCapabilityId, - Capabilities = new ModelCapabilities - { - Id = _nextCapabilityId++, - SupportsImageGeneration = true, - MaxTokens = 4000, - TokenizerType = TokenizerType.Cl100KBase - } - }; - } - - /// - /// Creates a DALL-E 2 model with image generation capabilities - /// - public static Model CreateDallE2Model(int? modelId = null) - { - return new Model - { - Id = modelId ?? _nextModelId++, - Name = "dall-e-2", - ModelSeriesId = 2, - ModelCapabilitiesId = _nextCapabilityId, - Capabilities = new ModelCapabilities - { - Id = _nextCapabilityId++, - SupportsImageGeneration = true, - MaxTokens = 1000, - TokenizerType = TokenizerType.Cl100KBase - } - }; - } - - /// - /// Creates a Stable Diffusion XL model - /// - public static Model CreateSDXLModel(int? modelId = null) - { - return new Model - { - Id = modelId ?? _nextModelId++, - Name = "sdxl", - ModelSeriesId = 3, - ModelCapabilitiesId = _nextCapabilityId, - Capabilities = new ModelCapabilities - { - Id = _nextCapabilityId++, - SupportsImageGeneration = true, - MaxTokens = 77, - TokenizerType = TokenizerType.Cl100KBase // Use a valid tokenizer type - } - }; - } - - /// - /// Creates a MiniMax image model - /// - public static Model CreateMiniMaxImageModel(int? modelId = null) - { - return new Model - { - Id = modelId ?? _nextModelId++, - Name = "minimax-image", - ModelSeriesId = 4, - ModelCapabilitiesId = _nextCapabilityId, - Capabilities = new ModelCapabilities - { - Id = _nextCapabilityId++, - SupportsImageGeneration = true, - MaxTokens = 1000, - TokenizerType = TokenizerType.Cl100KBase // Use a valid tokenizer type - } - }; - } - - /// - /// Creates a video generation model - /// - public static Model CreateVideoModel(int? modelId = null, string? name = null) - { - return new Model - { - Id = modelId ?? _nextModelId++, - Name = name ?? "video-generation-model", - ModelSeriesId = 5, - ModelCapabilitiesId = _nextCapabilityId, - Capabilities = new ModelCapabilities - { - Id = _nextCapabilityId++, - SupportsVideoGeneration = true, - MaxTokens = 1000, - TokenizerType = TokenizerType.Cl100KBase - } - }; - } - - /// - /// Creates model series for OpenAI models - /// - public static ModelSeries CreateOpenAISeries() - { - return new ModelSeries - { - Id = 1, - Name = "OpenAI", - Description = "OpenAI model series", - Author = new ModelAuthor - { - Id = 1, - Name = "OpenAI" - } - }; - } - - /// - /// Creates model series for DALL-E models - /// - public static ModelSeries CreateDallESeries() - { - return new ModelSeries - { - Id = 2, - Name = "DALL-E", - Description = "DALL-E image generation models", - Author = new ModelAuthor - { - Id = 1, - Name = "OpenAI" - } - }; - } - - /// - /// Creates model series for Stable Diffusion models - /// - public static ModelSeries CreateStableDiffusionSeries() - { - return new ModelSeries - { - Id = 3, - Name = "Stable Diffusion", - Description = "Stable Diffusion models", - Author = new ModelAuthor - { - Id = 2, - Name = "Stability AI" - } - }; - } - - /// - /// Creates model series for MiniMax models - /// - public static ModelSeries CreateMiniMaxSeries() - { - return new ModelSeries - { - Id = 4, - Name = "MiniMax", - Description = "MiniMax models", - Author = new ModelAuthor - { - Id = 3, - Name = "MiniMax" - } - }; - } - - /// - /// Creates a ModelProviderMapping for a given model - /// - public static ModelProviderMapping CreateMapping(Model model, int providerId, string? modelAlias = null) - { - return new ModelProviderMapping - { - Id = _nextModelId++, - ModelId = model.Id, - Model = model, - ModelAlias = modelAlias ?? model.Name, - ProviderModelId = model.Name, - ProviderId = providerId, - IsEnabled = true - }; - } - - /// - /// Creates a complete model with author, series, and capabilities for testing - /// - public static Model CreateCompleteTestModel( - string? modelName = null, - bool supportsChat = true, - bool supportsVideoGeneration = false, - int maxTokens = 4096) - { - var modelId = _nextModelId++; - var capabilityId = _nextCapabilityId++; - var authorId = _nextModelId++; - var seriesId = _nextModelId++; - - return new Model - { - Id = modelId, - Name = modelName ?? $"test-model-{modelId}", - ModelSeriesId = seriesId, - ModelCapabilitiesId = capabilityId, - Series = new ModelSeries - { - Id = seriesId, - Name = $"Test Series {seriesId}", - AuthorId = authorId, - Author = new ModelAuthor - { - Id = authorId, - Name = $"Test Author {authorId}" - }, - TokenizerType = TokenizerType.Cl100KBase - }, - Capabilities = new ModelCapabilities - { - Id = capabilityId, - SupportsChat = supportsChat, - SupportsVideoGeneration = supportsVideoGeneration, - MaxTokens = maxTokens, - TokenizerType = TokenizerType.Cl100KBase - } - }; - } - - /// - /// Reset ID counters for test isolation - /// - public static void ResetCounters() - { - _nextModelId = 1000; - _nextCapabilityId = 1000; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Authentication/VirtualKeyAuthenticationHandlerTests.cs b/ConduitLLM.Tests/Http/Authentication/VirtualKeyAuthenticationHandlerTests.cs deleted file mode 100644 index 905672515..000000000 --- a/ConduitLLM.Tests/Http/Authentication/VirtualKeyAuthenticationHandlerTests.cs +++ /dev/null @@ -1,297 +0,0 @@ -using System.Security.Claims; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using Xunit.Abstractions; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Http.Authentication; - -namespace ConduitLLM.Tests.Http.Authentication -{ - /// - /// Unit tests for VirtualKeyAuthenticationHandler - /// - public class VirtualKeyAuthenticationHandlerTests : TestBase - { - private readonly Mock _virtualKeyServiceMock; - private readonly Mock> _optionsMock; - private readonly Mock _loggerFactoryMock; - private readonly Mock> _loggerMock; - private readonly VirtualKeyAuthenticationHandler _handler; - private readonly DefaultHttpContext _httpContext; - - public VirtualKeyAuthenticationHandlerTests(ITestOutputHelper output) : base(output) - { - _virtualKeyServiceMock = new Mock(); - _optionsMock = new Mock>(); - _loggerFactoryMock = new Mock(); - _loggerMock = CreateLogger(); - - _loggerFactoryMock.Setup(f => f.CreateLogger(It.IsAny())) - .Returns(_loggerMock.Object); - - _optionsMock.Setup(o => o.Get(It.IsAny())) - .Returns(new AuthenticationSchemeOptions()); - - _handler = new VirtualKeyAuthenticationHandler( - _optionsMock.Object, - _loggerFactoryMock.Object, - UrlEncoder.Default, - _virtualKeyServiceMock.Object); - - _httpContext = new DefaultHttpContext(); - } - - [Fact] - public async Task HandleAuthenticateAsync_WithValidBearerToken_ReturnsSuccessResult() - { - // Arrange - var virtualKey = CreateValidVirtualKey(); - var keyValue = "condt_test123"; - - _httpContext.Request.Headers["Authorization"] = $"Bearer {keyValue}"; - _httpContext.Request.Path = "/v1/chat/completions"; - - _virtualKeyServiceMock.Setup(s => s.ValidateVirtualKeyForAuthenticationAsync(keyValue, null)) - .ReturnsAsync(virtualKey); - - await InitializeHandler(); - - // Act - var result = await _handler.AuthenticateAsync(); - - // Assert - Assert.True(result.Succeeded); - Assert.NotNull(result.Principal); - Assert.Equal("VirtualKey", result.Principal.Identity.AuthenticationType); - - var claims = result.Principal.Claims; - Assert.Contains(claims, c => c.Type == ClaimTypes.Name && c.Value == "Test Key"); - Assert.Contains(claims, c => c.Type == "VirtualKeyId" && c.Value == "1"); - Assert.Contains(claims, c => c.Type == "VirtualKey" && c.Value == keyValue); - - // Verify context items are set - Assert.Equal(1, _httpContext.Items["VirtualKeyId"]); - Assert.Equal(keyValue, _httpContext.Items["VirtualKey"]); - Assert.NotNull(_httpContext.Items["RequestStartTime"]); - - _virtualKeyServiceMock.Verify(s => s.ValidateVirtualKeyForAuthenticationAsync(keyValue, null), Times.Once); - } - - [Fact] - public async Task HandleAuthenticateAsync_WithValidApiKeyHeader_ReturnsSuccessResult() - { - // Arrange - var virtualKey = CreateValidVirtualKey(); - var keyValue = "condt_test456"; - - _httpContext.Request.Headers["X-API-Key"] = keyValue; - _httpContext.Request.Path = "/v1/models"; - - _virtualKeyServiceMock.Setup(s => s.ValidateVirtualKeyForAuthenticationAsync(keyValue, null)) - .ReturnsAsync(virtualKey); - - await InitializeHandler(); - - // Act - var result = await _handler.AuthenticateAsync(); - - // Assert - Assert.True(result.Succeeded); - Assert.NotNull(result.Principal); - _virtualKeyServiceMock.Verify(s => s.ValidateVirtualKeyForAuthenticationAsync(keyValue, null), Times.Once); - } - - [Fact] - public async Task HandleAuthenticateAsync_WithMissingVirtualKey_ReturnsFailure() - { - // Arrange - _httpContext.Request.Path = "/v1/chat/completions"; - // No Authorization or X-API-Key header - - await InitializeHandler(); - - // Act - var result = await _handler.AuthenticateAsync(); - - // Assert - Assert.False(result.Succeeded); - Assert.Equal("Missing Virtual Key", result.Failure.Message); - - _virtualKeyServiceMock.Verify(s => s.ValidateVirtualKeyForAuthenticationAsync(It.IsAny(), It.IsAny()), Times.Never); - } - - [Fact] - public async Task HandleAuthenticateAsync_WithInvalidVirtualKey_ReturnsFailure() - { - // Arrange - var keyValue = "invalid_key"; - - _httpContext.Request.Headers["Authorization"] = $"Bearer {keyValue}"; - _httpContext.Request.Path = "/v1/chat/completions"; - - _virtualKeyServiceMock.Setup(s => s.ValidateVirtualKeyForAuthenticationAsync(keyValue, null)) - .ReturnsAsync((VirtualKey?)null); - - await InitializeHandler(); - - // Act - var result = await _handler.AuthenticateAsync(); - - // Assert - Assert.False(result.Succeeded); - Assert.Equal("Invalid Virtual Key", result.Failure.Message); - - _virtualKeyServiceMock.Verify(s => s.ValidateVirtualKeyForAuthenticationAsync(keyValue, null), Times.Once); - } - - [Fact] - public async Task HandleAuthenticateAsync_WithZeroBalanceKey_ReturnsSuccess() - { - // Arrange - This tests our fix where authentication succeeds even with $0.00 balance - var virtualKey = CreateValidVirtualKey(); // Valid key but potentially $0.00 balance - var keyValue = "condt_zerobalance"; - - _httpContext.Request.Headers["Authorization"] = $"Bearer {keyValue}"; - _httpContext.Request.Path = "/v1/models/gpt-4/metadata"; - - // ValidateVirtualKeyForAuthenticationAsync should succeed even with $0.00 balance - _virtualKeyServiceMock.Setup(s => s.ValidateVirtualKeyForAuthenticationAsync(keyValue, null)) - .ReturnsAsync(virtualKey); - - await InitializeHandler(); - - // Act - var result = await _handler.AuthenticateAsync(); - - // Assert - Assert.True(result.Succeeded); // Authentication succeeds regardless of balance - Assert.NotNull(result.Principal); - - _virtualKeyServiceMock.Verify(s => s.ValidateVirtualKeyForAuthenticationAsync(keyValue, null), Times.Once); - } - - [Fact] - public async Task HandleAuthenticateAsync_WithExcludedPath_ReturnsAnonymousSuccess() - { - // Arrange - _httpContext.Request.Path = "/health"; - // No authentication headers required for excluded paths - - await InitializeHandler(); - - // Act - var result = await _handler.AuthenticateAsync(); - - // Assert - Assert.True(result.Succeeded); - Assert.NotNull(result.Principal); - Assert.False(result.Principal.Identity.IsAuthenticated); // Anonymous identity - - // Verify service was never called for excluded paths - _virtualKeyServiceMock.Verify(s => s.ValidateVirtualKeyForAuthenticationAsync(It.IsAny(), It.IsAny()), Times.Never); - } - - [Theory] - [InlineData("/health")] - [InlineData("/health/ready")] - [InlineData("/health/live")] - [InlineData("/metrics")] - [InlineData("/v1/media/public")] - public async Task HandleAuthenticateAsync_WithExcludedPaths_SkipsAuthentication(string path) - { - // Arrange - _httpContext.Request.Path = path; - - await InitializeHandler(); - - // Act - var result = await _handler.AuthenticateAsync(); - - // Assert - Assert.True(result.Succeeded); - Assert.False(result.Principal.Identity.IsAuthenticated); - - _virtualKeyServiceMock.Verify(s => s.ValidateVirtualKeyForAuthenticationAsync(It.IsAny(), It.IsAny()), Times.Never); - } - - [Fact] - public async Task HandleAuthenticateAsync_WithSignalRConnection_ExtractsFromQueryString() - { - // Arrange - var virtualKey = CreateValidVirtualKey(); - var keyValue = "condt_signalr"; - - _httpContext.Request.Path = "/hubs/chat"; - _httpContext.Request.QueryString = new QueryString($"?access_token={keyValue}"); - - _virtualKeyServiceMock.Setup(s => s.ValidateVirtualKeyForAuthenticationAsync(keyValue, null)) - .ReturnsAsync(virtualKey); - - await InitializeHandler(); - - // Act - var result = await _handler.AuthenticateAsync(); - - // Assert - Assert.True(result.Succeeded); - Assert.NotNull(result.Principal); - - _virtualKeyServiceMock.Verify(s => s.ValidateVirtualKeyForAuthenticationAsync(keyValue, null), Times.Once); - } - - [Fact] - public async Task HandleAuthenticateAsync_WithServiceException_ReturnsFailure() - { - // Arrange - var keyValue = "condt_exception"; - - _httpContext.Request.Headers["Authorization"] = $"Bearer {keyValue}"; - _httpContext.Request.Path = "/v1/chat/completions"; - - _virtualKeyServiceMock.Setup(s => s.ValidateVirtualKeyForAuthenticationAsync(keyValue, null)) - .ThrowsAsync(new Exception("Database connection failed")); - - await InitializeHandler(); - - // Act - var result = await _handler.AuthenticateAsync(); - - // Assert - Assert.False(result.Succeeded); - Assert.Equal("Authentication error", result.Failure.Message); - - _virtualKeyServiceMock.Verify(s => s.ValidateVirtualKeyForAuthenticationAsync(keyValue, null), Times.Once); - } - - /// - /// Creates a valid virtual key for testing - /// - private VirtualKey CreateValidVirtualKey() - { - return new VirtualKey - { - Id = 1, - KeyName = "Test Key", - KeyHash = "test-hash", - IsEnabled = true, - VirtualKeyGroupId = 1, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }; - } - - /// - /// Initializes the handler with the HTTP context - /// - private async Task InitializeHandler() - { - var scheme = new AuthenticationScheme("VirtualKey", "Virtual Key", typeof(VirtualKeyAuthenticationHandler)); - await _handler.InitializeAsync(scheme, _httpContext); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Builders/ModelProviderMappingBuilder.cs b/ConduitLLM.Tests/Http/Builders/ModelProviderMappingBuilder.cs deleted file mode 100644 index 6d6a7d2ec..000000000 --- a/ConduitLLM.Tests/Http/Builders/ModelProviderMappingBuilder.cs +++ /dev/null @@ -1,292 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Http.Builders -{ - public class ModelProviderMappingBuilder - { - private static int _idCounter = 0; - private ModelProviderMapping _mapping; - private Provider? _provider; - private Model? _model; - private ModelCapabilities? _capabilities; - private ModelSeries? _series; - - public ModelProviderMappingBuilder() - { - var baseId = Interlocked.Increment(ref _idCounter); - - _series = new ModelSeries - { - Id = baseId * 10 + 1, - Name = "GPT-4", - Parameters = "{}" - }; - - _capabilities = new ModelCapabilities - { - Id = baseId * 10 + 2, - MaxTokens = 4096, - MinTokens = 1, - TokenizerType = TokenizerType.Cl100KBase, - SupportsChat = true, - SupportsStreaming = false, - SupportsVision = false, - SupportsFunctionCalling = false, - SupportsAudioTranscription = false, - SupportsTextToSpeech = false, - SupportsRealtimeAudio = false, - SupportsVideoGeneration = false, - SupportsImageGeneration = false, - SupportsEmbeddings = false - }; - - _model = new Model - { - Id = baseId * 10 + 3, - Name = "GPT-4", - Version = "v1", - Description = "Test model", - ModelCardUrl = "https://example.com/model", - ModelSeriesId = _series.Id, - Series = _series, - ModelCapabilitiesId = _capabilities.Id, - Capabilities = _capabilities, - IsActive = true - }; - - _provider = new Provider - { - Id = baseId * 10 + 4, - ProviderName = "OpenAI", - ProviderType = ProviderType.OpenAI, - IsEnabled = true, - BaseUrl = "https://api.openai.com" - }; - - _mapping = new ModelProviderMapping - { - Id = baseId * 10 + 5, - ModelAlias = "test-model", - ProviderModelId = "test-model-id", - ProviderId = _provider.Id, - Provider = _provider, - IsEnabled = true, - ModelId = _model.Id, - Model = _model, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }; - } - - public ModelProviderMappingBuilder WithModelAlias(string alias) - { - _mapping.ModelAlias = alias; - return this; - } - - public ModelProviderMappingBuilder WithProviderModelId(string id) - { - _mapping.ProviderModelId = id; - return this; - } - - public ModelProviderMappingBuilder WithProviderId(int id) - { - _mapping.ProviderId = id; - return this; - } - - public ModelProviderMappingBuilder WithProvider(Provider provider) - { - _provider = provider; - _mapping.Provider = provider; - _mapping.ProviderId = provider.Id; - return this; - } - - public ModelProviderMappingBuilder WithMappingEnabled(bool isEnabled) - { - _mapping.IsEnabled = isEnabled; - return this; - } - - public ModelProviderMappingBuilder WithProviderEnabled(bool isEnabled) - { - if (_provider != null) - { - _provider.IsEnabled = isEnabled; - } - return this; - } - - public ModelProviderMappingBuilder WithModelId(int id) - { - _mapping.ModelId = id; - if (_model != null) - { - _model.Id = id; - } - return this; - } - - public ModelProviderMappingBuilder WithModel(Model? model) - { - _model = model; - _mapping.Model = model!; - if (model != null) - { - _mapping.ModelId = model.Id; - } - return this; - } - - public ModelProviderMappingBuilder WithCapabilities(ModelCapabilities? capabilities) - { - _capabilities = capabilities; - if (_model != null && capabilities != null) - { - _model.Capabilities = capabilities; - _model.ModelCapabilitiesId = capabilities.Id; - } - return this; - } - - public ModelProviderMappingBuilder WithVisionSupport(bool supports) - { - if (_capabilities != null) - { - _capabilities.SupportsVision = supports; - } - return this; - } - - public ModelProviderMappingBuilder WithStreamingSupport(bool supports) - { - if (_capabilities != null) - { - _capabilities.SupportsStreaming = supports; - } - return this; - } - - public ModelProviderMappingBuilder WithFullCapabilities() - { - if (_capabilities != null) - { - _capabilities.SupportsChat = true; - _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; - } - return this; - } - - public ModelProviderMappingBuilder WithDescription(string? description) - { - if (_model != null) - { - _model.Description = description; - } - return this; - } - - public ModelProviderMappingBuilder WithModelCardUrl(string? url) - { - if (_model != null) - { - _model.ModelCardUrl = url; - } - return this; - } - - public ModelProviderMappingBuilder WithMaxTokens(int maxTokens) - { - if (_capabilities != null) - { - _capabilities.MaxTokens = maxTokens; - } - return this; - } - - public ModelProviderMappingBuilder WithTokenizerType(TokenizerType type) - { - if (_capabilities != null) - { - _capabilities.TokenizerType = type; - } - return this; - } - - public ModelProviderMappingBuilder WithSeriesParameters(string parameters) - { - if (_series != null) - { - _series.Parameters = parameters; - } - return this; - } - - public ModelProviderMappingBuilder WithSeriesName(string name) - { - if (_series != null) - { - _series.Name = name; - } - return this; - } - - public ModelProviderMapping Build() - { - // Return a new instance to prevent mutation - var mapping = new ModelProviderMapping - { - Id = _mapping.Id, - ModelAlias = _mapping.ModelAlias, - ProviderModelId = _mapping.ProviderModelId, - ProviderId = _mapping.ProviderId, - Provider = _provider!, - IsEnabled = _mapping.IsEnabled, - ModelId = _mapping.ModelId, - Model = _model!, - CreatedAt = _mapping.CreatedAt, - UpdatedAt = _mapping.UpdatedAt, - MaxContextTokensOverride = _mapping.MaxContextTokensOverride, - CapabilityOverrides = _mapping.CapabilityOverrides, - IsDefault = _mapping.IsDefault, - DefaultCapabilityType = _mapping.DefaultCapabilityType, - ProviderVariation = _mapping.ProviderVariation, - QualityScore = _mapping.QualityScore - }; - - // Ensure the Model has the correct references - if (_model != null) - { - mapping.Model = new Model - { - Id = _model.Id, - Name = _model.Name, - Version = _model.Version, - Description = _model.Description, - ModelCardUrl = _model.ModelCardUrl, - ModelSeriesId = _model.ModelSeriesId, - Series = _series!, - ModelCapabilitiesId = _model.ModelCapabilitiesId, - Capabilities = _capabilities!, - IsActive = _model.IsActive, - ModelParameters = _model.ModelParameters, - CreatedAt = _model.CreatedAt, - UpdatedAt = _model.UpdatedAt - }; - } - - return mapping; - } - } -} \ No newline at end of file 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/GetModels/GetModelsResponseStructureTests.cs b/ConduitLLM.Tests/Http/Controllers/Discovery/GetModels/GetModelsResponseStructureTests.cs deleted file mode 100644 index dfbf06ea6..000000000 --- a/ConduitLLM.Tests/Http/Controllers/Discovery/GetModels/GetModelsResponseStructureTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Tests.Http.Builders; -using Xunit.Abstractions; - -namespace ConduitLLM.Tests.Http.Controllers.Discovery.GetModels -{ - [Trait("Category", "Unit")] - [Trait("Component", "Http")] - [Trait("Phase", "2")] - public class GetModelsResponseStructureTests : DiscoveryControllerTestsBase - { - public GetModelsResponseStructureTests(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public async Task GetModels_ReturnsFlatStructureWithBooleanCapabilityFlags() - { - // Arrange - SetupValidVirtualKey("valid-key"); - - var mappings = new List - { - new ModelProviderMappingBuilder() - .WithModelAlias("gpt-4") - .WithFullCapabilities() - .Build() - }; - - SetupModelProviderMappings(mappings); - - // Act - var result = await Controller.GetModels(); - - // Assert - var okResult = Assert.IsType(result); - dynamic response = okResult.Value!; - dynamic model = ((IEnumerable)response.data).First(); - - Assert.True(model.supports_chat); - 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); - } - - [Fact] - public async Task GetModels_IncludesMetadataFields_ReturnsCompleteModelInfo() - { - // Arrange - SetupValidVirtualKey("valid-key"); - - var mappings = new List - { - new ModelProviderMappingBuilder() - .WithModelAlias("gpt-4") - .WithDescription("Advanced language model") - .WithModelCardUrl("https://example.com/gpt-4") - .WithMaxTokens(8192) - .WithTokenizerType(TokenizerType.Cl100KBase) - .Build() - }; - - SetupModelProviderMappings(mappings); - - // Act - var result = await Controller.GetModels(); - - // Assert - var okResult = Assert.IsType(result); - dynamic response = okResult.Value!; - dynamic model = ((IEnumerable)response.data).First(); - - Assert.Equal("gpt-4", model.id); - Assert.Equal("gpt-4", model.display_name); - Assert.Equal("Advanced language model", model.description); - Assert.Equal("https://example.com/gpt-4", model.model_card_url); - Assert.Equal(8192, model.max_tokens); - Assert.Equal("cl100kbase", model.tokenizer_type); - } - - [Fact] - public async Task GetModels_HandlesNullDescriptionAndModelCardUrl_ReturnsEmptyStrings() - { - // Arrange - SetupValidVirtualKey("valid-key"); - - var mappings = new List - { - new ModelProviderMappingBuilder() - .WithModelAlias("gpt-4") - .WithDescription(null) - .WithModelCardUrl(null) - .Build() - }; - - SetupModelProviderMappings(mappings); - - // Act - var result = await Controller.GetModels(); - - // Assert - var okResult = Assert.IsType(result); - dynamic response = okResult.Value!; - dynamic model = ((IEnumerable)response.data).First(); - - Assert.Equal(string.Empty, model.description); - Assert.Equal(string.Empty, model.model_card_url); - } - } -} \ No newline at end of file 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/Http/Middleware/UsageTrackingMiddlewareTests.EdgeCases.cs b/ConduitLLM.Tests/Http/Middleware/UsageTrackingMiddlewareTests.EdgeCases.cs deleted file mode 100644 index e94911da8..000000000 --- a/ConduitLLM.Tests/Http/Middleware/UsageTrackingMiddlewareTests.EdgeCases.cs +++ /dev/null @@ -1,264 +0,0 @@ -using ConduitLLM.Core.Models; -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Http.Middleware; -using Microsoft.Extensions.Logging; -using Moq; - -namespace ConduitLLM.Tests.Http.Middleware -{ - public partial class UsageTrackingMiddlewareTests - { - [Fact] - public async Task Streaming_Response_Uses_StreamingUsage_From_Context() - { - // Arrange - var context = CreateHttpContext("/v1/chat/completions"); - var virtualKeyId = 654; - var virtualKey = "test-key-654"; - - context.Items["VirtualKeyId"] = virtualKeyId; - context.Items["VirtualKey"] = virtualKey; - context.Items["IsStreamingRequest"] = true; - context.Items["ProviderType"] = "OpenAI"; - context.Response.ContentType = "text/event-stream"; - - // Simulate SSE writer storing usage data - var streamingUsage = new Usage - { - PromptTokens = 50, - CompletionTokens = 150, - TotalTokens = 200 - }; - context.Items["StreamingUsage"] = streamingUsage; - context.Items["StreamingModel"] = "gpt-4"; - - _mockCostService.Setup(x => x.CalculateCostAsync("gpt-4", It.IsAny(), default)) - .ReturnsAsync(0.006m); - - _mockBatchSpendService.SetupGet(x => x.IsHealthy).Returns(true); - - // Act - await _middleware.InvokeAsync(context, _mockCostService.Object, _mockBatchSpendService.Object, - _mockRequestLogService.Object, _mockVirtualKeyService.Object, _mockBillingAuditService.Object); - - // Assert - _mockCostService.Verify(x => x.CalculateCostAsync("gpt-4", - It.Is(u => u.PromptTokens == 50 && u.CompletionTokens == 150), - default), Times.Once); - - _mockBatchSpendService.Verify(x => x.QueueSpendUpdate(virtualKeyId, 0.006m), Times.Once); - } - - [Fact] - public async Task BatchSpendService_Unhealthy_Falls_Back_To_Direct_Update() - { - // Arrange - var context = CreateHttpContext("/v1/chat/completions"); - var virtualKeyId = 987; - var virtualKey = "test-key-987"; - - context.Items["VirtualKeyId"] = virtualKeyId; - context.Items["VirtualKey"] = virtualKey; - - var response = new - { - model = "gpt-3.5-turbo", - usage = new { prompt_tokens = 10, completion_tokens = 20, total_tokens = 30 } - }; - - SetupMockResponse(context, response); - - _mockCostService.Setup(x => x.CalculateCostAsync("gpt-3.5-turbo", It.IsAny(), default)) - .ReturnsAsync(0.0001m); - - // Batch service is unhealthy - _mockBatchSpendService.SetupGet(x => x.IsHealthy).Returns(false); - - // Create a new middleware instance that uses our updated _next delegate - var middleware = new UsageTrackingMiddleware(_next, _mockLogger.Object); - - // Act - await middleware.InvokeAsync(context, _mockCostService.Object, _mockBatchSpendService.Object, - _mockRequestLogService.Object, _mockVirtualKeyService.Object, _mockBillingAuditService.Object); - - // Assert - _mockBatchSpendService.Verify(x => x.QueueSpendUpdate(It.IsAny(), It.IsAny()), Times.Never); - _mockVirtualKeyService.Verify(x => x.UpdateSpendAsync(virtualKeyId, 0.0001m), Times.Once); - } - - [Fact] - public async Task Zero_Cost_Does_Not_Update_Spend() - { - // Arrange - var context = CreateHttpContext("/v1/chat/completions"); - var virtualKeyId = 111; - var virtualKey = "test-key-111"; - - context.Items["VirtualKeyId"] = virtualKeyId; - context.Items["VirtualKey"] = virtualKey; - - var response = new - { - model = "test-free-model", - usage = new { prompt_tokens = 10, completion_tokens = 20, total_tokens = 30 } - }; - - SetupMockResponse(context, response); - - _mockCostService.Setup(x => x.CalculateCostAsync("test-free-model", It.IsAny(), default)) - .ReturnsAsync(0m); - - // Act - await _middleware.InvokeAsync(context, _mockCostService.Object, _mockBatchSpendService.Object, - _mockRequestLogService.Object, _mockVirtualKeyService.Object, _mockBillingAuditService.Object); - - // Assert - _mockBatchSpendService.Verify(x => x.QueueSpendUpdate(It.IsAny(), It.IsAny()), Times.Never); - _mockVirtualKeyService.Verify(x => x.UpdateSpendAsync(It.IsAny(), It.IsAny()), Times.Never); - } - - [Fact] - public async Task Non_API_Request_Skips_Tracking() - { - // Arrange - var context = CreateHttpContext("/health"); - - // Act - await _middleware.InvokeAsync(context, _mockCostService.Object, _mockBatchSpendService.Object, - _mockRequestLogService.Object, _mockVirtualKeyService.Object, _mockBillingAuditService.Object); - - // Assert - _mockCostService.Verify(x => x.CalculateCostAsync(It.IsAny(), It.IsAny(), default), Times.Never); - _mockBatchSpendService.Verify(x => x.QueueSpendUpdate(It.IsAny(), It.IsAny()), Times.Never); - } - - [Fact] - public async Task Error_Response_Skips_Tracking() - { - // Arrange - var context = CreateHttpContext("/v1/chat/completions", 400); - context.Items["VirtualKeyId"] = 123; - context.Items["VirtualKey"] = "test-key"; - - // Act - await _middleware.InvokeAsync(context, _mockCostService.Object, _mockBatchSpendService.Object, - _mockRequestLogService.Object, _mockVirtualKeyService.Object, _mockBillingAuditService.Object); - - // Assert - No cost calculation or spend update should occur - _mockCostService.Verify(x => x.CalculateCostAsync(It.IsAny(), It.IsAny(), default), Times.Never); - _mockBatchSpendService.Verify(x => x.QueueSpendUpdate(It.IsAny(), It.IsAny()), Times.Never); - - // Assert - Debug log should indicate billing was skipped due to error response - _mockLogger.Verify( - x => x.Log( - Microsoft.Extensions.Logging.LogLevel.Debug, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Billing Policy: Skipping billing for error response") && - v.ToString().Contains("Status=400") && - v.ToString().Contains("Reason=ErrorResponse_NoChargePolicy")), - It.IsAny(), - It.IsAny>()), - Times.Once); - } - - [Theory] - [InlineData(400)] // Bad Request - [InlineData(401)] // Unauthorized - [InlineData(404)] // Not Found - [InlineData(429)] // Rate Limited - [InlineData(500)] // Internal Server Error - [InlineData(503)] // Service Unavailable - public async Task Billing_Policy_Skips_All_Error_Status_Codes(int statusCode) - { - // Arrange - var context = CreateHttpContext("/v1/chat/completions", statusCode); - context.Items["VirtualKeyId"] = 123; - context.Items["VirtualKey"] = "test-key"; - - // Act - await _middleware.InvokeAsync(context, _mockCostService.Object, _mockBatchSpendService.Object, - _mockRequestLogService.Object, _mockVirtualKeyService.Object, _mockBillingAuditService.Object); - - // Assert - No billing should occur for any error status - _mockCostService.Verify(x => x.CalculateCostAsync(It.IsAny(), It.IsAny(), default), Times.Never); - _mockBatchSpendService.Verify(x => x.QueueSpendUpdate(It.IsAny(), It.IsAny()), Times.Never); - - // Assert - Appropriate debug logging - _mockLogger.Verify( - x => x.Log( - Microsoft.Extensions.Logging.LogLevel.Debug, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Billing Policy: Skipping billing for error response") && - v.ToString().Contains($"Status={statusCode}") && - v.ToString().Contains("Reason=ErrorResponse_NoChargePolicy")), - It.IsAny(), - It.IsAny>()), - Times.Once); - } - - [Fact] - public async Task Missing_VirtualKey_Skips_Tracking() - { - // Arrange - var context = CreateHttpContext("/v1/chat/completions"); - // Don't add VirtualKeyId to context - - // Act - await _middleware.InvokeAsync(context, _mockCostService.Object, _mockBatchSpendService.Object, - _mockRequestLogService.Object, _mockVirtualKeyService.Object, _mockBillingAuditService.Object); - - // Assert - _mockCostService.Verify(x => x.CalculateCostAsync(It.IsAny(), It.IsAny(), default), Times.Never); - - // Assert - Debug log should indicate no virtual key - _mockLogger.Verify( - x => x.Log( - Microsoft.Extensions.Logging.LogLevel.Debug, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Billing Policy: Skipping billing - no virtual key found") && - v.ToString().Contains("Reason=NoVirtualKey")), - It.IsAny(), - It.IsAny>()), - Times.Once); - } - - [Fact] - public async Task Response_Time_Tracking() - { - // Arrange - var context = CreateHttpContext("/v1/chat/completions"); - var virtualKeyId = 555; - var virtualKey = "test-key-555"; - var startTime = DateTime.UtcNow.AddMilliseconds(-250); // 250ms ago - - context.Items["VirtualKeyId"] = virtualKeyId; - context.Items["VirtualKey"] = virtualKey; - context.Items["RequestStartTime"] = startTime; - - var response = new - { - model = "gpt-4", - usage = new { prompt_tokens = 10, completion_tokens = 20, total_tokens = 30 } - }; - - SetupMockResponse(context, response); - - _mockCostService.Setup(x => x.CalculateCostAsync("gpt-4", It.IsAny(), default)) - .ReturnsAsync(0.001m); - - _mockBatchSpendService.SetupGet(x => x.IsHealthy).Returns(true); - - // Create a new middleware instance that uses our updated _next delegate - var middleware = new UsageTrackingMiddleware(_next, _mockLogger.Object); - - // Act - await middleware.InvokeAsync(context, _mockCostService.Object, _mockBatchSpendService.Object, - _mockRequestLogService.Object, _mockVirtualKeyService.Object, _mockBillingAuditService.Object); - - // Assert - _mockRequestLogService.Verify(x => x.LogRequestAsync(It.Is(dto => - dto.ResponseTimeMs >= 250 && dto.ResponseTimeMs <= 500 // Allow some tolerance - )), Times.Once); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Middleware/UsageTrackingMiddlewareTests.Media.cs b/ConduitLLM.Tests/Http/Middleware/UsageTrackingMiddlewareTests.Media.cs deleted file mode 100644 index 919ed550c..000000000 --- a/ConduitLLM.Tests/Http/Middleware/UsageTrackingMiddlewareTests.Media.cs +++ /dev/null @@ -1,66 +0,0 @@ -using ConduitLLM.Core.Models; -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Http.Middleware; -using Moq; - -namespace ConduitLLM.Tests.Http.Middleware -{ - public partial class UsageTrackingMiddlewareTests - { - [Fact] - public async Task OpenAI_ImageGeneration_Response_Tracks_Usage() - { - // Arrange - var context = CreateHttpContext("/v1/images/generations"); - var virtualKeyId = 321; - var virtualKey = "test-key-321"; - - context.Items["VirtualKeyId"] = virtualKeyId; - context.Items["VirtualKey"] = virtualKey; - context.Items["ProviderType"] = "OpenAI"; - - var imageResponse = new - { - created = 1677652288, - model = "dall-e-3", - data = new[] - { - new - { - url = "https://example.com/image1.png", - revised_prompt = "A futuristic city with flying cars" - } - }, - usage = new - { - images = 1 - } - }; - - SetupMockResponse(context, imageResponse); - - _mockCostService.Setup(x => x.CalculateCostAsync("dall-e-3", It.IsAny(), default)) - .ReturnsAsync(0.04m); - - _mockBatchSpendService.SetupGet(x => x.IsHealthy).Returns(true); - - // Create a new middleware instance that uses our updated _next delegate - var middleware = new UsageTrackingMiddleware(_next, _mockLogger.Object); - - // Act - await middleware.InvokeAsync(context, _mockCostService.Object, _mockBatchSpendService.Object, - _mockRequestLogService.Object, _mockVirtualKeyService.Object, _mockBillingAuditService.Object); - - // Assert - _mockCostService.Verify(x => x.CalculateCostAsync("dall-e-3", - It.Is(u => u.ImageCount == 1), - default), Times.Once); - - _mockBatchSpendService.Verify(x => x.QueueSpendUpdate(virtualKeyId, 0.04m), Times.Once); - - _mockRequestLogService.Verify(x => x.LogRequestAsync(It.Is(dto => - dto.RequestType == "image" - )), Times.Once); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Middleware/UsageTrackingMiddlewareTests.Providers.cs b/ConduitLLM.Tests/Http/Middleware/UsageTrackingMiddlewareTests.Providers.cs deleted file mode 100644 index f56d45626..000000000 --- a/ConduitLLM.Tests/Http/Middleware/UsageTrackingMiddlewareTests.Providers.cs +++ /dev/null @@ -1,191 +0,0 @@ -using ConduitLLM.Core.Models; -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Http.Middleware; -using Moq; - -namespace ConduitLLM.Tests.Http.Middleware -{ - public partial class UsageTrackingMiddlewareTests - { - [Fact] - public async Task OpenAI_ChatCompletion_Response_Tracks_Usage() - { - // Arrange - var context = CreateHttpContext("/v1/chat/completions"); - var virtualKeyId = 123; - var virtualKey = "test-key-123"; - - context.Items["VirtualKeyId"] = virtualKeyId; - context.Items["VirtualKey"] = virtualKey; - context.Items["ProviderType"] = "OpenAI"; - - var openAiResponse = new - { - id = "chatcmpl-123", - @object = "chat.completion", - created = 1677652288, - model = "gpt-4", - choices = new[] - { - new - { - index = 0, - message = new { role = "assistant", content = "Hello! How can I help you?" }, - finish_reason = "stop" - } - }, - usage = new - { - prompt_tokens = 9, - completion_tokens = 12, - total_tokens = 21 - } - }; - - SetupMockResponse(context, openAiResponse); - - _mockCostService.Setup(x => x.CalculateCostAsync("gpt-4", It.IsAny(), default)) - .ReturnsAsync(0.001m); - - _mockBatchSpendService.SetupGet(x => x.IsHealthy).Returns(true); - - // Create a new middleware instance that uses our updated _next delegate - var middleware = new UsageTrackingMiddleware(_next, _mockLogger.Object); - - // Act - await middleware.InvokeAsync(context, _mockCostService.Object, _mockBatchSpendService.Object, - _mockRequestLogService.Object, _mockVirtualKeyService.Object, _mockBillingAuditService.Object); - - // Assert - _mockCostService.Verify(x => x.CalculateCostAsync("gpt-4", - It.Is(u => u.PromptTokens == 9 && u.CompletionTokens == 12), - default), Times.Once); - - _mockBatchSpendService.Verify(x => x.QueueSpendUpdate(virtualKeyId, 0.001m), Times.Once); - - _mockRequestLogService.Verify(x => x.LogRequestAsync(It.Is(dto => - dto.VirtualKeyId == virtualKeyId && - dto.ModelName == "gpt-4" && - dto.InputTokens == 9 && - dto.OutputTokens == 12 && - dto.Cost == 0.001m && - dto.RequestType == "chat" - )), Times.Once); - } - - [Fact] - public async Task Anthropic_ChatCompletion_Response_Tracks_Usage() - { - // Arrange - var context = CreateHttpContext("/v1/chat/completions"); - var virtualKeyId = 456; - var virtualKey = "test-key-456"; - - context.Items["VirtualKeyId"] = virtualKeyId; - context.Items["VirtualKey"] = virtualKey; - context.Items["ProviderType"] = "Anthropic"; - - var anthropicResponse = new - { - id = "msg_01XhEY9K2nPNTxWZj5vZ2VBm", - type = "message", - role = "assistant", - model = "claude-3-5-sonnet-20241022", - content = new[] - { - new { type = "text", text = "Hello! I'm Claude, an AI assistant created by Anthropic." } - }, - stop_reason = "end_turn", - stop_sequence = (string?)null, - usage = new - { - input_tokens = 2095, - output_tokens = 503, - cache_creation_input_tokens = 0, - cache_read_input_tokens = 0 - } - }; - - SetupMockResponse(context, anthropicResponse); - - _mockCostService.Setup(x => x.CalculateCostAsync("claude-3-5-sonnet-20241022", It.IsAny(), default)) - .ReturnsAsync(0.015m); - - _mockBatchSpendService.SetupGet(x => x.IsHealthy).Returns(true); - - // Create a new middleware instance that uses our updated _next delegate - var middleware = new UsageTrackingMiddleware(_next, _mockLogger.Object); - - // Act - await middleware.InvokeAsync(context, _mockCostService.Object, _mockBatchSpendService.Object, - _mockRequestLogService.Object, _mockVirtualKeyService.Object, _mockBillingAuditService.Object); - - // Assert - _mockCostService.Verify(x => x.CalculateCostAsync("claude-3-5-sonnet-20241022", - It.Is(u => - u.PromptTokens == 2095 && - u.CompletionTokens == 503 && - u.CachedWriteTokens == 0 && - u.CachedInputTokens == 0), - default), Times.Once); - - _mockBatchSpendService.Verify(x => x.QueueSpendUpdate(virtualKeyId, 0.015m), Times.Once); - } - - [Fact] - public async Task Anthropic_WithCachedTokens_Tracks_Usage() - { - // Arrange - var context = CreateHttpContext("/v1/chat/completions"); - var virtualKeyId = 789; - var virtualKey = "test-key-789"; - - context.Items["VirtualKeyId"] = virtualKeyId; - context.Items["VirtualKey"] = virtualKey; - context.Items["ProviderType"] = "Anthropic"; - - var anthropicResponse = new - { - id = "msg_01XhEY9K2nPNTxWZj5vZ2VBm", - type = "message", - role = "assistant", - model = "claude-3-5-sonnet-20241022", - content = new[] - { - new { type = "text", text = "Based on our previous discussion..." } - }, - stop_reason = "end_turn", - usage = new - { - input_tokens = 500, - output_tokens = 100, - cache_creation_input_tokens = 1500, - cache_read_input_tokens = 2000 - } - }; - - SetupMockResponse(context, anthropicResponse); - - _mockCostService.Setup(x => x.CalculateCostAsync("claude-3-5-sonnet-20241022", It.IsAny(), default)) - .ReturnsAsync(0.008m); - - _mockBatchSpendService.SetupGet(x => x.IsHealthy).Returns(true); - - // Create a new middleware instance that uses our updated _next delegate - var middleware = new UsageTrackingMiddleware(_next, _mockLogger.Object); - - // Act - await middleware.InvokeAsync(context, _mockCostService.Object, _mockBatchSpendService.Object, - _mockRequestLogService.Object, _mockVirtualKeyService.Object, _mockBillingAuditService.Object); - - // Assert - _mockCostService.Verify(x => x.CalculateCostAsync("claude-3-5-sonnet-20241022", - It.Is(u => - u.PromptTokens == 500 && - u.CompletionTokens == 100 && - u.CachedWriteTokens == 1500 && - u.CachedInputTokens == 2000), - default), Times.Once); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Middleware/UsageTrackingMiddlewareTests.cs b/ConduitLLM.Tests/Http/Middleware/UsageTrackingMiddlewareTests.cs deleted file mode 100644 index d22302dda..000000000 --- a/ConduitLLM.Tests/Http/Middleware/UsageTrackingMiddlewareTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Text; -using System.Text.Json; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Moq; -using ConduitLLM.Http.Middleware; -using ConduitLLM.Core.Interfaces; -using IVirtualKeyService = ConduitLLM.Core.Interfaces.IVirtualKeyService; -using ConduitLLM.Configuration.Interfaces; - -namespace ConduitLLM.Tests.Http.Middleware -{ - public partial class UsageTrackingMiddlewareTests - { - private readonly Mock _mockCostService; - private readonly Mock _mockBatchSpendService; - private readonly Mock _mockRequestLogService; - private readonly Mock _mockVirtualKeyService; - private readonly Mock _mockBillingAuditService; - private readonly Mock> _mockLogger; - private readonly UsageTrackingMiddleware _middleware; - private RequestDelegate _next; - - public UsageTrackingMiddlewareTests() - { - _mockCostService = new Mock(); - _mockBatchSpendService = new Mock(); - _mockRequestLogService = new Mock(); - _mockVirtualKeyService = new Mock(); - _mockBillingAuditService = new Mock(); - _mockLogger = new Mock>(); - - // Default _next delegate - will be replaced by SetupMockResponse - _next = (HttpContext ctx) => Task.CompletedTask; - _middleware = new UsageTrackingMiddleware(_next, _mockLogger.Object); - } - - private HttpContext CreateHttpContext(string path, int statusCode = 200) - { - var context = new DefaultHttpContext(); - context.Request.Path = path; - context.Response.StatusCode = statusCode; - context.Response.Body = new MemoryStream(); - return context; - } - - private void SetupMockResponse(HttpContext context, object responseData) - { - var json = JsonSerializer.Serialize(responseData); - var bytes = Encoding.UTF8.GetBytes(json); - - // Replace the _next delegate to write the response - _next = async (HttpContext ctx) => - { - // Set response properties first - ctx.Response.ContentType = "application/json"; - ctx.Response.StatusCode = 200; - ctx.Response.ContentLength = bytes.Length; - - // Write the response data to the response body - await ctx.Response.Body.WriteAsync(bytes, 0, bytes.Length); - }; - } - } -} \ 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/Integration/VirtualKeyBalanceTrackingTests.cs b/ConduitLLM.Tests/Integration/VirtualKeyBalanceTrackingTests.cs deleted file mode 100644 index 84f1ea86a..000000000 --- a/ConduitLLM.Tests/Integration/VirtualKeyBalanceTrackingTests.cs +++ /dev/null @@ -1,278 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.Enums; -using ConduitLLM.Configuration.Interfaces; -using ConduitLLM.Configuration.Repositories; - -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -using Moq; - -namespace ConduitLLM.Tests.Integration -{ - /// - /// Integration tests for virtual key balance tracking to ensure - /// transactions are recorded correctly with proper balance calculations - /// - [Trait("Category", "Integration")] - [Trait("Component", "VirtualKeyBalanceTracking")] - public class VirtualKeyBalanceTrackingTests : IDisposable - { - private readonly IConfigurationDbContext _dbContext; - private readonly ConduitDbContext _concreteDbContext; - private readonly VirtualKeyGroupRepository _repository; - private readonly Mock> _mockLogger; - - public VirtualKeyBalanceTrackingTests() - { - // Setup in-memory database for integration testing - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) - .Options; - _concreteDbContext = new ConduitDbContext(options); - _dbContext = _concreteDbContext; - - _mockLogger = new Mock>(); - _repository = new VirtualKeyGroupRepository(_concreteDbContext, _mockLogger.Object); - } - - [Fact] - public async Task AdjustBalance_ForDebit_ShouldCreateCorrectTransaction() - { - // Arrange - var initialBalance = 100m; - var usageAmount = 15.75m; - var expectedBalance = initialBalance - usageAmount; - - var group = new VirtualKeyGroup - { - GroupName = "Test Group", - Balance = initialBalance, - LifetimeCreditsAdded = initialBalance, - LifetimeSpent = 0 - }; - - var groupId = await _repository.CreateAsync(group); - - // Act - var newBalance = await _repository.AdjustBalanceAsync( - groupId, - -usageAmount, - "API usage test", - "TestUser"); - - // Assert - Assert.Equal(expectedBalance, newBalance); - - // Verify the group was updated correctly - var updatedGroup = await _repository.GetByIdAsync(groupId); - Assert.NotNull(updatedGroup); - Assert.Equal(expectedBalance, updatedGroup.Balance); - Assert.Equal(usageAmount, updatedGroup.LifetimeSpent); - Assert.Equal(initialBalance, updatedGroup.LifetimeCreditsAdded); - - // Verify transaction was created with correct values - var transactions = await _dbContext.VirtualKeyGroupTransactions - .Where(t => t.VirtualKeyGroupId == groupId && t.TransactionType == TransactionType.Debit) - .ToListAsync(); - - Assert.Single(transactions); - var transaction = transactions.First(); - - Assert.Equal(TransactionType.Debit, transaction.TransactionType); - Assert.Equal(usageAmount, transaction.Amount); // Should be positive - Assert.Equal(expectedBalance, transaction.BalanceAfter); // Should match new balance - Assert.Equal("API usage test", transaction.Description); - Assert.Equal("TestUser", transaction.InitiatedBy); - Assert.Equal(ReferenceType.Manual, transaction.ReferenceType); - } - - [Fact] - public async Task AdjustBalance_ForCredit_ShouldCreateCorrectTransaction() - { - // Arrange - var initialBalance = 50m; - var creditAmount = 25m; - var expectedBalance = initialBalance + creditAmount; - - var group = new VirtualKeyGroup - { - GroupName = "Test Group", - Balance = initialBalance, - LifetimeCreditsAdded = initialBalance, - LifetimeSpent = 10m - }; - - var groupId = await _repository.CreateAsync(group); - - // Act - var newBalance = await _repository.AdjustBalanceAsync( - groupId, - creditAmount, - "Credits added", - "AdminUser"); - - // Assert - Assert.Equal(expectedBalance, newBalance); - - // Verify the group was updated correctly - var updatedGroup = await _repository.GetByIdAsync(groupId); - Assert.NotNull(updatedGroup); - Assert.Equal(expectedBalance, updatedGroup.Balance); - Assert.Equal(10m, updatedGroup.LifetimeSpent); // Should not change - Assert.Equal(initialBalance + creditAmount, updatedGroup.LifetimeCreditsAdded); - - // Verify transaction was created with correct values (excluding initial balance transaction) - var transactions = await _dbContext.VirtualKeyGroupTransactions - .Where(t => t.VirtualKeyGroupId == groupId - && t.TransactionType == TransactionType.Credit - && t.ReferenceType != ReferenceType.Initial) - .OrderByDescending(t => t.CreatedAt) - .ToListAsync(); - - Assert.Single(transactions); - var transaction = transactions.First(); - - Assert.Equal(TransactionType.Credit, transaction.TransactionType); - Assert.Equal(creditAmount, transaction.Amount); // Should be positive - Assert.Equal(expectedBalance, transaction.BalanceAfter); // Should match new balance - Assert.Equal("Credits added", transaction.Description); - Assert.Equal("AdminUser", transaction.InitiatedBy); - } - - [Fact] - public async Task MultipleAdjustments_ShouldMaintainCorrectBalanceHistory() - { - // Arrange - var initialBalance = 100m; - var group = new VirtualKeyGroup - { - GroupName = "Test Group", - Balance = initialBalance, - LifetimeCreditsAdded = initialBalance, - LifetimeSpent = 0 - }; - - var groupId = await _repository.CreateAsync(group); - - // Act - Perform multiple transactions - // Transaction 1: Debit $10 - await _repository.AdjustBalanceAsync(groupId, -10m, "Usage 1", "System"); - - // Transaction 2: Debit $5.50 - await _repository.AdjustBalanceAsync(groupId, -5.50m, "Usage 2", "System"); - - // Transaction 3: Credit $20 - await _repository.AdjustBalanceAsync(groupId, 20m, "Refill", "Admin"); - - // Transaction 4: Debit $2.25 - await _repository.AdjustBalanceAsync(groupId, -2.25m, "Usage 3", "System"); - - // Assert - var finalGroup = await _repository.GetByIdAsync(groupId); - var expectedFinalBalance = initialBalance - 10m - 5.50m + 20m - 2.25m; - Assert.Equal(expectedFinalBalance, finalGroup.Balance); - Assert.Equal(17.75m, finalGroup.LifetimeSpent); // 10 + 5.50 + 2.25 - Assert.Equal(120m, finalGroup.LifetimeCreditsAdded); // 100 + 20 - - // Verify all transactions have correct BalanceAfter values - var allTransactions = await _dbContext.VirtualKeyGroupTransactions - .Where(t => t.VirtualKeyGroupId == groupId) - .OrderBy(t => t.CreatedAt) - .ToListAsync(); - - // Should have 5 transactions (1 initial + 4 adjustments) - Assert.Equal(5, allTransactions.Count); - - // Check each transaction has the correct balance after - Assert.Equal(100m, allTransactions[0].BalanceAfter); // Initial - Assert.Equal(90m, allTransactions[1].BalanceAfter); // After -10 - Assert.Equal(84.50m, allTransactions[2].BalanceAfter); // After -5.50 - Assert.Equal(104.50m, allTransactions[3].BalanceAfter); // After +20 - Assert.Equal(102.25m, allTransactions[4].BalanceAfter); // After -2.25 - } - - [Fact] - public async Task AdjustBalance_WithNullDescription_ShouldUseDefaultDescription() - { - // Arrange - var group = new VirtualKeyGroup - { - GroupName = "Test Group", - Balance = 100m, - LifetimeCreditsAdded = 100m, - LifetimeSpent = 0 - }; - - var groupId = await _repository.CreateAsync(group); - - // Act - Debit without description - await _repository.AdjustBalanceAsync(groupId, -10m, null, "System"); - - // Act - Credit without description - await _repository.AdjustBalanceAsync(groupId, 5m, null, "System"); - - // Assert - var transactions = await _dbContext.VirtualKeyGroupTransactions - .Where(t => t.VirtualKeyGroupId == groupId) - .OrderBy(t => t.CreatedAt) - .ToListAsync(); - - // Find the debit and credit transactions (skip initial) - var debitTransaction = transactions.FirstOrDefault(t => t.TransactionType == TransactionType.Debit); - var creditTransaction = transactions.FirstOrDefault(t => t.TransactionType == TransactionType.Credit && t.Amount == 5m); - - Assert.NotNull(debitTransaction); - Assert.NotNull(creditTransaction); - Assert.Equal("Usage deducted", debitTransaction.Description); - Assert.Equal("Credits added", creditTransaction.Description); - } - - [Fact] - public async Task CreateGroup_WithInitialBalance_ShouldCreateInitialTransaction() - { - // Arrange & Act - var initialBalance = 50m; - var group = new VirtualKeyGroup - { - GroupName = "New Group", - Balance = initialBalance, - LifetimeCreditsAdded = initialBalance, - LifetimeSpent = 0 - }; - - var groupId = await _repository.CreateAsync(group); - - // Assert - var transactions = await _dbContext.VirtualKeyGroupTransactions - .Where(t => t.VirtualKeyGroupId == groupId) - .ToListAsync(); - - Assert.Single(transactions); - var initialTransaction = transactions.First(); - - Assert.Equal(TransactionType.Credit, initialTransaction.TransactionType); - Assert.Equal(initialBalance, initialTransaction.Amount); - Assert.Equal(initialBalance, initialTransaction.BalanceAfter); - Assert.Equal("Initial balance", initialTransaction.Description); - Assert.Equal(ReferenceType.Initial, initialTransaction.ReferenceType); - } - - [Fact] - public async Task AdjustBalance_ForNonExistentGroup_ShouldThrowException() - { - // Arrange - var nonExistentGroupId = 9999; - - // Act & Assert - await Assert.ThrowsAsync(async () => - await _repository.AdjustBalanceAsync(nonExistentGroupId, -10m, "Test", "User")); - } - - public void Dispose() - { - _concreteDbContext?.Dispose(); - } - } -} \ 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/README.md b/ConduitLLM.Tests/README.md deleted file mode 100644 index e12fde17e..000000000 --- a/ConduitLLM.Tests/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# ConduitLLM.Tests - -## Overview -`ConduitLLM.Tests` is the automated test suite for the ConduitLLM solution, which is managed via the top-level `Conduit.sln` file. This project ensures the reliability, correctness, and maintainability of the core ConduitLLM components by providing comprehensive unit and integration tests. - -## How It Fits Into the Conduit Solution -The ConduitLLM solution is a modular .NET-based framework for working with Large Language Models (LLMs). It is composed of several sub-projects, including: - -- **ConduitLLM.Core**: Core abstractions, interfaces, and shared logic for LLM operations. -- **ConduitLLM.Http**: Provides HTTP APIs for LLM operations. -- **ConduitLLM.WebUI**: Web-based user interface for interacting with LLMs. -- **ConduitLLM.Configuration**: Centralized configuration management. -- **ConduitLLM.Providers**: Integrations for various LLM providers (OpenAI, Cohere, Gemini, Anthropic, LiteLLM, etc.). -- **ConduitLLM.Tests**: This project. Contains tests for all core, provider, HTTP, and configuration components. - -All these projects are orchestrated via `Conduit.sln`, allowing for coordinated development and testing. - -## What Does ConduitLLM.Tests Do? -- **Covers all major components**: Tests core logic, provider integrations, API endpoints, configuration, caching, streaming, security, and middleware. -- **Ensures provider compatibility**: Validates correct interaction with OpenAI, Cohere, Gemini, Anthropic, and LiteLLM clients. -- **Supports CI/CD**: Can be run as part of automated pipelines to prevent regressions. - -## Test Structure -The test project is organized as follows: - -- `*.cs` files: Test suites for specific providers or features (e.g., `OpenAIClientTests.cs`, `GeminiClientTests.cs`, `StreamingTests.cs`). -- `Caching/`, `Middleware/`, `Security/`, `Services/`, `TestHelpers/`: Subdirectories for organizing tests and helpers by concern. - -## How to Run the Tests -You can run all tests using the .NET CLI: - -```bash -# From the solution root -dotnet test ConduitLLM.Tests/ConduitLLM.Tests.csproj -``` - -Or run all tests in the solution: - -```bash -dotnet test Conduit.sln -``` - -## Configuration -Most tests run with default settings and use in-memory or mock services. However, some integration tests may expect certain environment variables or configuration files to be set, especially when testing real LLM provider APIs. See the following for guidance: - -- **Environment Variables**: Provider API keys (e.g., `OPENAI_API_KEY`, `COHERE_API_KEY`, etc.) may be required for integration tests. -- **Ports**: The test suite does not bind to HTTP/HTTPS ports by default, but if you run the full solution, refer to the main project documentation for port configuration. - -## Adding or Modifying Tests -- Place new test files in the appropriate directory or create a new one if needed. -- Follow the naming convention: `[Component]Tests.cs`. -- Use xUnit (or the test framework specified in `.csproj`). -- Use mocks or stubs for external dependencies where possible. - -## Best Practices -- Keep tests isolated and repeatable. -- Clean up any resources created during tests. -- Prefer in-memory or mock services for unit tests; use real services only for explicit integration tests. - -## Additional Information -- For details on the core logic, see `ConduitLLM.Core/README.md`. -- For provider-specific info, see `ConduitLLM.Providers/README.md`. -- For API usage, see `ConduitLLM.Http/README.md`. -- For configuration, see `ConduitLLM.Configuration/README.md`. - -## Contact & Support -For questions, issues, or contributions, please refer to the main ConduitLLM repository documentation or open an issue on the project's GitHub page. 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.Tests/TestHelpers/MockBuilders.cs b/ConduitLLM.Tests/TestHelpers/MockBuilders.cs deleted file mode 100644 index 5bd9f98e9..000000000 --- a/ConduitLLM.Tests/TestHelpers/MockBuilders.cs +++ /dev/null @@ -1,294 +0,0 @@ -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; - -using Moq; - -namespace ConduitLLM.Tests.TestHelpers -{ - /// - /// Fluent builders for creating complex mock setups. - /// - public static class MockBuilders - { - /// - /// Creates a fluent builder for IDistributedCache mock. - /// - public static DistributedCacheBuilder BuildDistributedCache() - { - return new DistributedCacheBuilder(); - } - - /// - /// Creates a fluent builder for ICacheService mock. - /// - public static CacheServiceBuilder BuildCacheService() - { - return new CacheServiceBuilder(); - } - - /// - /// Creates a fluent builder for complex memory cache scenarios. - /// - public static MemoryCacheBuilder BuildMemoryCache() - { - return new MemoryCacheBuilder(); - } - } - - /// - /// Fluent builder for IDistributedCache mocks. - /// - public class DistributedCacheBuilder - { - private readonly Mock _mock = new(); - private readonly Dictionary _cache = new(); - - public DistributedCacheBuilder WithValue(string key, string value) - { - var bytes = System.Text.Encoding.UTF8.GetBytes(value); - _cache[key] = bytes; - return this; - } - - public DistributedCacheBuilder WithValue(string key, byte[] value) - { - _cache[key] = value; - return this; - } - - public DistributedCacheBuilder WithGetBehavior() - { - _mock.Setup(x => x.Get(It.IsAny())) - .Returns((string key) => _cache.TryGetValue(key, out var value) ? value : null); - - _mock.Setup(x => x.GetAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync((string key, CancellationToken ct) => - _cache.TryGetValue(key, out var value) ? value : null); - - return this; - } - - public DistributedCacheBuilder WithSetBehavior() - { - _mock.Setup(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((string key, byte[] value, DistributedCacheEntryOptions options) => _cache[key] = value); - - _mock.Setup(x => x.SetAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken ct) => - { - _cache[key] = value; - return Task.CompletedTask; - }); - - return this; - } - - public DistributedCacheBuilder WithRemoveBehavior() - { - _mock.Setup(x => x.Remove(It.IsAny())) - .Callback((string key) => _cache.Remove(key)); - - _mock.Setup(x => x.RemoveAsync(It.IsAny(), It.IsAny())) - .Returns((string key, CancellationToken ct) => - { - _cache.Remove(key); - return Task.CompletedTask; - }); - - return this; - } - - public DistributedCacheBuilder WithFullBehavior() - { - return WithGetBehavior().WithSetBehavior().WithRemoveBehavior(); - } - - public Mock Build() - { - return _mock; - } - } - - /// - /// Fluent builder for ICacheService mocks (ConduitLLM.Configuration.Services). - /// - public class CacheServiceBuilder - { - private readonly Mock _mock = new(); - private readonly Dictionary _cache = new(); - private TimeSpan? _defaultExpiration; - - public CacheServiceBuilder WithDefaultExpiration(TimeSpan expiration) - { - _defaultExpiration = expiration; - return this; - } - - public CacheServiceBuilder WithCachedValue(string key, T value) - { - _cache[key] = value; - return this; - } - - public CacheServiceBuilder WithGetBehavior() - { - _mock.Setup(x => x.Get(It.IsAny())) - .Returns((string key) => - { - if (_cache.TryGetValue(key, out var value)) - { - return value; - } - return null; - }); - - return this; - } - - public CacheServiceBuilder WithSetBehavior() - { - _mock.Setup(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((string key, object value, TimeSpan? absoluteExpiration, TimeSpan? slidingExpiration) => - { - _cache[key] = value; - }); - - return this; - } - - public CacheServiceBuilder WithGetOrCreateBehavior(Func> factory = null) - { - _mock.Setup(x => x.GetOrCreateAsync( - It.IsAny(), - It.IsAny>>(), - It.IsAny(), - It.IsAny())) - .Returns(async (string key, Func> valueFactory, TimeSpan? absoluteExpiration, TimeSpan? slidingExpiration) => - { - if (_cache.TryGetValue(key, out var cached) && cached is T typedValue) - { - return typedValue; - } - - var value = factory != null ? await factory(key) : await valueFactory(); - _cache[key] = value; - return value; - }); - - return this; - } - - public Mock Build() - { - return _mock; - } - } - - /// - /// Fluent builder for complex IMemoryCache scenarios. - /// - public class MemoryCacheBuilder - { - private readonly Mock _mock = new(); - private readonly Dictionary _cache = new(); - private readonly List _entries = new(); - - private class CacheItem - { - public object Value { get; set; } - public DateTimeOffset? AbsoluteExpiration { get; set; } - public TimeSpan? SlidingExpiration { get; set; } - } - - public MemoryCacheBuilder WithEntry(object key, object value, Action configure = null) - { - var item = new CacheItem { Value = value }; - _cache[key] = item; - - var entry = new Mock(); - entry.SetupAllProperties(); - entry.Setup(x => x.Key).Returns(key); - entry.Setup(x => x.Value).Returns(() => item.Value); - entry.SetupSet(x => x.Value = It.IsAny()).Callback(v => item.Value = v); - entry.SetupSet(x => x.AbsoluteExpiration = It.IsAny()) - .Callback(exp => item.AbsoluteExpiration = exp); - entry.SetupSet(x => x.SlidingExpiration = It.IsAny()) - .Callback(exp => item.SlidingExpiration = exp); - - configure?.Invoke(entry.Object); - _entries.Add(entry.Object); - - return this; - } - - public MemoryCacheBuilder WithEvictionCallback(Action callback) - { - foreach (var entry in _entries) - { - var mockEntry = Mock.Get(entry); - mockEntry.Setup(x => x.PostEvictionCallbacks).Returns(new List - { - new PostEvictionCallbackRegistration - { - EvictionCallback = (key, value, reason, state) => callback(key, value, reason, state), - State = null - } - }); - } - return this; - } - - public MemoryCacheBuilder WithSizeLimit(long sizeLimit) - { - // This would require a more complex implementation with size tracking - return this; - } - - public Mock Build() - { - _mock.Setup(x => x.TryGetValue(It.IsAny(), out It.Ref.IsAny)) - .Returns((object key, out object value) => - { - if (_cache.TryGetValue(key, out var item)) - { - // Check expiration - if (item.AbsoluteExpiration.HasValue && item.AbsoluteExpiration.Value <= DateTimeOffset.UtcNow) - { - _cache.Remove(key); - value = null; - return false; - } - value = item.Value; - return true; - } - value = null; - return false; - }); - - _mock.Setup(x => x.CreateEntry(It.IsAny())) - .Returns((object key) => - { - var entry = _entries.FirstOrDefault(e => e.Key.Equals(key)); - if (entry != null) return entry; - - var newEntry = new Mock(); - newEntry.SetupAllProperties(); - newEntry.Setup(e => e.Key).Returns(key); - - var item = _cache.ContainsKey(key) ? _cache[key] : new CacheItem(); - _cache[key] = item; - - newEntry.Setup(e => e.Value).Returns(() => item.Value); - newEntry.SetupSet(e => e.Value = It.IsAny()) - .Callback(v => item.Value = v); - - return newEntry.Object; - }); - - _mock.Setup(x => x.Remove(It.IsAny())) - .Callback((object key) => _cache.Remove(key)); - - return _mock; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/.eslintrc.json b/ConduitLLM.WebUI/.eslintrc.json deleted file mode 100755 index ab63c29cc..000000000 --- a/ConduitLLM.WebUI/.eslintrc.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "extends": [ - "next/core-web-vitals", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json", - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint", - "eslint-comments" - ], - "rules": { - "@typescript-eslint/no-unused-vars": "error", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/no-non-null-assertion": "warn", - "@typescript-eslint/strict-boolean-expressions": "off", - "@typescript-eslint/no-floating-promises": "warn", - "@typescript-eslint/no-misused-promises": "warn", - "@typescript-eslint/await-thenable": "warn", - "@typescript-eslint/no-unsafe-assignment": "error", - "@typescript-eslint/no-unsafe-member-access": "error", - "@typescript-eslint/no-unsafe-call": "error", - "@typescript-eslint/no-unsafe-return": "error", - "@typescript-eslint/require-await": "off", - "@typescript-eslint/no-unnecessary-type-assertion": "warn", - "@typescript-eslint/prefer-nullish-coalescing": "warn", - "@typescript-eslint/prefer-optional-chain": "warn", - "@typescript-eslint/no-unsafe-argument": "error", - "@typescript-eslint/consistent-type-assertions": [ - "error", - { - "assertionStyle": "as", - "objectLiteralTypeAssertions": "allow" - } - ], - "@typescript-eslint/naming-convention": [ - "error", - { - "selector": "variable", - "format": ["camelCase", "PascalCase", "UPPER_CASE", "snake_case"], - "leadingUnderscore": "forbid" - }, - { - "selector": "parameter", - "format": ["camelCase", "PascalCase", "snake_case"], - "leadingUnderscore": "forbid" - }, - { - "selector": "property", - "format": ["camelCase", "PascalCase", "UPPER_CASE", "snake_case"], - "leadingUnderscore": "forbid", - "filter": { - "regex": "^(Content-Type|Content-Disposition|content-type|content-disposition|max_tokens|top_p|presence_penalty|response_format|aspect_ratio|webhook_url|supportsFunctionCalling|supportsVision|supportsImageGeneration|supportsAudioTranscription|supportsTextToSpeech|supportsRealtimeAudio|supportsStreaming|supportsVideoGeneration|supportsEmbeddings|maxContextLength|maxOutputTokens|isDefault|defaultCapabilityType|_note)$", - "match": false - } - } - ], - "eslint-comments/no-unlimited-disable": "error", - "eslint-comments/no-unused-disable": "error", - "eslint-comments/disable-enable-pair": "error", - "eslint-comments/no-duplicate-disable": "error", - "eslint-comments/no-restricted-disable": [ - "error", - "@typescript-eslint/no-unsafe-*", - "@typescript-eslint/naming-convention", - "@typescript-eslint/no-explicit-any" - ], - "no-console": ["warn", { "allow": ["warn", "error"] }], - "eqeqeq": ["error", "always"], - "no-debugger": "error", - "no-alert": "error", - "no-var": "error", - "prefer-const": "error", - "prefer-arrow-callback": "error", - "no-param-reassign": "error", - "no-return-await": "error", - "no-nested-ternary": "warn", - "no-unneeded-ternary": "error", - "prefer-template": "error", - "no-duplicate-imports": "warn" - }, - "overrides": [ - { - "files": ["*.js"], - "rules": { - "@typescript-eslint/explicit-function-return-type": "off" - } - }, - { - "files": ["**/__tests__/**/*", "**/*.test.*", "**/*.spec.*"], - "env": { - "jest": true - } - } - ], - "ignorePatterns": [ - ".next/", - "out/", - "node_modules/", - "*.js", - "!*.config.js" - ] -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/Dockerfile b/ConduitLLM.WebUI/Dockerfile deleted file mode 100755 index c46fc0555..000000000 --- a/ConduitLLM.WebUI/Dockerfile +++ /dev/null @@ -1,82 +0,0 @@ -# Optimized multi-stage Dockerfile for ConduitLLM.WebUI -# Features: Multi-stage build, Alpine base, non-root user, health checks - -FROM node:22-alpine AS builder -WORKDIR /app - -# IMPORTANT: This is a monorepo where packages depend on each other via file: references -# The WebUI package.json contains: -# "@knn_labs/conduit-admin-client": "file:../SDKs/Node/Admin" -# "@knn_labs/conduit-core-client": "file:../SDKs/Node/Core" -# Therefore, we MUST copy the entire monorepo structure before running npm install -# Otherwise npm will timeout trying to fetch these packages from the registry -COPY . . - -# Configure npm for ARM64 compatibility (increase timeouts for slower I/O) -RUN npm config set fetch-timeout 600000 && \ - npm config set fetch-retry-mintimeout 20000 && \ - npm config set fetch-retry-maxtimeout 120000 && \ - npm config set registry https://registry.npmjs.org/ - -# Build Common package first (base dependency for other packages) -WORKDIR /app/SDKs/Node/Common -RUN npm install --no-audit --no-fund --verbose || (cat /root/.npm/_logs/*.log 2>/dev/null && exit 1) -RUN npm run build - -# Build Admin SDK next (depends on Common package) -WORKDIR /app/SDKs/Node/Admin -RUN npm install --no-audit --no-fund --verbose || (cat /root/.npm/_logs/*.log 2>/dev/null && exit 1) -RUN npm run build - -# Build Core SDK next (depends on Common package) -WORKDIR /app/SDKs/Node/Core -RUN npm install --no-audit --no-fund --verbose || (cat /root/.npm/_logs/*.log 2>/dev/null && exit 1) -RUN npm run build - -# Build WebUI last (depends on Admin and Core SDKs via file: references) -# The WebUI's npm install will symlink to the local Admin and Core packages -WORKDIR /app/ConduitLLM.WebUI -# Create public directory if it doesn't exist (Next.js 15 doesn't require it) -RUN mkdir -p public -# npm install here will resolve file: dependencies to the already-built SDKs above -RUN npm install --no-audit --no-fund --verbose || (cat /root/.npm/_logs/*.log 2>/dev/null && exit 1) -RUN npm run build - -# Production stage - smaller final image -FROM node:22-alpine AS runner -WORKDIR /app - -# Create non-root user for security -RUN addgroup -g 1001 -S nodejs && \ - adduser -S nextjs -u 1001 - -# Set production environment -ENV NODE_ENV=production -ENV PORT=3000 -ENV HOSTNAME="0.0.0.0" -ENV NEXT_TELEMETRY_DISABLED=1 - -# Copy only what's needed from builder -COPY --from=builder --chown=nextjs:nodejs /app/ConduitLLM.WebUI/package*.json ./ -COPY --from=builder --chown=nextjs:nodejs /app/ConduitLLM.WebUI/.next ./.next -COPY --from=builder --chown=nextjs:nodejs /app/ConduitLLM.WebUI/node_modules ./node_modules -COPY --from=builder --chown=nextjs:nodejs /app/ConduitLLM.WebUI/public ./public - -# IMPORTANT: We must also copy the SDK packages because WebUI's node_modules contains -# symlinks to these local packages (due to file: references in package.json) -# Without these, the runtime will fail to resolve the packages -COPY --from=builder --chown=nextjs:nodejs /app/SDKs/Node/Common /app/SDKs/Node/Common -COPY --from=builder --chown=nextjs:nodejs /app/SDKs/Node/Admin /app/SDKs/Node/Admin -COPY --from=builder --chown=nextjs:nodejs /app/SDKs/Node/Core /app/SDKs/Node/Core - -# Switch to non-root user -USER nextjs - -EXPOSE 3000 - -# Health check for container orchestration -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget -q -O /dev/null http://localhost:3000/api/health || exit 1 - -# Start the application -CMD ["npm", "start"] \ No newline at end of file diff --git a/ConduitLLM.WebUI/README.md b/ConduitLLM.WebUI/README.md deleted file mode 100755 index 3022f98bd..000000000 --- a/ConduitLLM.WebUI/README.md +++ /dev/null @@ -1,271 +0,0 @@ -# Conduit WebUI - -Next.js-based web interface for the Conduit LLM Platform, built with React, TypeScript, and Mantine. - -## Architecture Overview - -The WebUI uses SDK React Query hooks directly for all API operations: - -### Client-Side SDK Usage -- **Core SDK**: Used for LLM operations (chat, images, video, audio) with virtual key authentication -- **Admin SDK**: Used for admin operations (providers, keys, settings) with master key authentication - -### Authentication Flow -1. Admin logs in through Clerk authentication -2. WebUI verifies user has `siteadmin: true` in Clerk metadata -3. Server uses `CONDUIT_API_TO_API_BACKEND_AUTH_KEY` for backend API calls -4. All admin operations use master key authentication server-side - -### Key Benefits -- 🚀 **Direct SDK Usage**: No proxy layer, reduced latency -- 🔄 **React Query Integration**: Built-in caching, optimistic updates -- 🔐 **Secure Authentication**: Virtual keys for client-side operations -- 📦 **Simplified Codebase**: Less code to maintain - -## Features - -- 🚀 **Next.js 15** with App Router and TypeScript -- 🎨 **Mantine UI** component library with custom theme -- 🔗 **Direct SDK Integration** with React Query hooks -- ⚡ **Real-time Updates** via SignalR -- 📊 **State Management** with Zustand and React Query -- 🎯 **Type Safety** throughout the application -- 🔐 **Automatic Virtual Key Management** for secure API access - -## Quick Start - -1. **Install dependencies:** - ```bash - npm install - ``` - -2. **Configure environment:** - ```bash - cp .env.example .env.local - # Edit .env.local with your configuration - ``` - -3. **Start development server:** - ```bash - npm run dev - ``` - -4. **Open in browser:** - ``` - http://localhost:3000 - ``` - -## Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `NEXT_PUBLIC_CONDUIT_ADMIN_API_URL` | Admin API endpoint | `http://localhost:5002` | -| `NEXT_PUBLIC_CONDUIT_CORE_API_URL` | Core API endpoint | `http://localhost:5000` | -| `CONDUIT_API_TO_API_BACKEND_AUTH_KEY` | Backend service authentication key | `alpha` | -| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Clerk publishable key for authentication | Required | -| `CLERK_SECRET_KEY` | Clerk secret key for authentication | Required | -| `NEXT_PUBLIC_ENABLE_REAL_TIME_UPDATES` | Enable SignalR features | `true` | - -## Project Structure - -``` -src/ -├── app/ # Next.js App Router pages -├── components/ # Reusable UI components -│ ├── ui/ # Base UI components -│ ├── forms/ # Form components -│ ├── charts/ # Data visualization -│ ├── layout/ # Layout components -│ └── realtime/ # SignalR components -├── hooks/ # Custom React hooks -│ ├── signalr/ # SignalR-specific hooks -│ └── api/ # API integration hooks -├── lib/ # Utilities and configurations -│ ├── clients/ # SDK client configurations -│ ├── auth/ # Authentication utilities -│ ├── signalr/ # SignalR connection management -│ └── utils/ # Helper functions -├── stores/ # Zustand stores -├── types/ # TypeScript type definitions -└── styles/ # Mantine theme customization -``` - -## Available Scripts - -- `npm run dev` - Start development server on port 3000 -- `npm run build` - Build for production -- `npm run start` - Start production server -- `npm run lint` - Run ESLint -- `npm run type-check` - Run TypeScript type checking - -## Development - -### Adding New Pages - -1. Create page component in `src/app/[page-name]/page.tsx` -2. Add navigation links in the layout components -3. Implement API integration using Conduit SDKs - -### Using SDK React Query Hooks - -The WebUI now uses SDK React Query hooks directly in components: - -```typescript -// Using Core SDK hooks -import { useChatCompletion, useImageGeneration } from '@knn_labs/conduit-core-client/react-query'; - -function ChatComponent() { - const { mutate: sendMessage } = useChatCompletion(); - - const handleSend = (messages) => { - sendMessage({ messages }); - }; -} - -// Using Admin SDK hooks -import { useProviders, useCreateProvider } from '@knn_labs/conduit-admin-client/react-query'; - -function ProvidersPage() { - const { data: providers } = useProviders(); - const { mutate: createProvider } = useCreateProvider(); -} -``` - -### Provider Setup - -SDK providers are configured in the app layout: - -```typescript -// lib/providers/ConduitProviders.tsx -import { ConduitProvider } from '@knn_labs/conduit-core-client/react-query'; -import { ConduitAdminProvider } from '@knn_labs/conduit-admin-client/react-query'; - -export function ConduitProviders({ children }) { - const { virtualKey } = useAuthStore(); - - return ( - - - {children} - - - ); -} -``` - -### Real-time Features - -SignalR connections are managed centrally and provide real-time updates for: -- Virtual key spend tracking -- Provider health monitoring -- Task progress (image/video generation) -- Navigation state updates - -## Video Generation - -The WebUI provides a comprehensive video generation interface with real-time progress tracking through the SDK's unified interface. - -### Features -- ✨ Real-time progress updates via SignalR -- 🔄 Automatic fallback to polling if connection fails -- 📊 Smooth progress bar animations -- 💬 Descriptive status messages -- 🎯 Queue management for multiple videos -- 🎨 Visual preview of generated videos - -### Usage - -```typescript -import { useVideoGeneration } from '@/app/videos/hooks/useVideoGeneration'; - -function VideoGenerator() { - const { generateVideo, isGenerating, error } = useVideoGeneration(); - - const handleGenerate = async () => { - await generateVideo({ - prompt: "A serene lake at sunset", - settings: { - model: "minimax-video-01", - duration: 6, - size: "1280x720", - fps: 30 - } - }); - }; - - return ( -
- - {error &&
Error: {error}
} -
- ); -} -``` - -### Progress Tracking - -The video generation hook automatically handles: -1. **SignalR Connection**: Establishes real-time connection for updates -2. **Progress Events**: Receives percentage, status, and messages -3. **Fallback Logic**: Switches to polling if SignalR fails -4. **State Management**: Updates UI with progress information - -### Configuration - -Enable/disable progress tracking features: - -```typescript -// Use the enhanced video generation with progress tracking -const { generateVideo } = useVideoGeneration({ - useProgressTracking: true, // Enable SDK progress tracking - fallbackToPolling: true, // Enable polling fallback -}); -``` - -### Video Queue - -The WebUI maintains a queue of video generation tasks: -- View all pending, running, and completed videos -- Cancel in-progress generations -- Download completed videos -- Retry failed generations - -## Docker Deployment - -The WebUI is configured to run as part of the ConduitLLM Docker stack: - -```bash -# Build and start all services from the root directory -docker-compose up -d - -# The WebUI will be available at http://localhost:3000 -``` - -### Docker Environment Variables - -The following environment variables are configured in docker-compose.yml: - -- `NEXT_PUBLIC_CONDUIT_CORE_API_URL`: Public URL for Core API (browser access) -- `NEXT_PUBLIC_CONDUIT_ADMIN_API_URL`: Public URL for Admin API (browser access) -- `CONDUIT_API_BASE_URL`: Internal URL for Core API (server-side) -- `CONDUIT_ADMIN_API_BASE_URL`: Internal URL for Admin API (server-side) -- `CONDUIT_API_EXTERNAL_URL`: External URL for SignalR Core API connections -- `CONDUIT_ADMIN_API_EXTERNAL_URL`: External URL for SignalR Admin API connections -- `CONDUIT_API_TO_API_BACKEND_AUTH_KEY`: Master key for Admin API authentication -- `SESSION_SECRET`: Secret key for session encryption -- `REDIS_URL`: Redis connection string for session storage - -The application runs on port 3000 in the Docker environment. - -## Contributing - -1. Follow the existing code patterns and conventions -2. Use TypeScript for all new code -3. Add appropriate tests for new features -4. Update documentation as needed - -## License - -ISC \ No newline at end of file diff --git a/ConduitLLM.WebUI/docs/README.md b/ConduitLLM.WebUI/docs/README.md deleted file mode 100755 index 89d29b5dc..000000000 --- a/ConduitLLM.WebUI/docs/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# Conduit WebUI Documentation - -## Overview - -This directory contains comprehensive documentation for the Conduit WebUI, including architecture details, migration guides, and security considerations. - -## Documentation Index - -### 📐 [Architecture](./ARCHITECTURE.md) -Detailed overview of the WebUI architecture, including: -- Architecture evolution from proxy-based to direct SDK -- Component architecture and data flow -- Authentication and security architecture -- Deployment architecture with Docker - -### 🔑 [Virtual Key Management](./VIRTUAL-KEY-MANAGEMENT.md) -Complete guide to WebUI virtual key system: -- How virtual keys work -- Automatic key creation and management -- Security model and best practices -- Key rotation and monitoring - -### 🔄 [Migration Guide](./MIGRATION-GUIDE.md) -Step-by-step guide for migrating from API routes to SDK hooks: -- Before and after code examples -- Provider setup instructions -- Common patterns and best practices -- Rollback strategies - -### 🔧 [Troubleshooting](./TROUBLESHOOTING.md) -Solutions to common issues: -- Authentication problems -- SDK hook issues -- Network and CORS errors -- Performance optimization -- Debugging tools and techniques - -### 🔒 [Security Considerations](./SECURITY-CONSIDERATIONS.md) -Important security information: -- Authentication key separation -- Virtual key exposure risks -- Security layers and best practices -- Incident response procedures - -### 🔐 [Security Authentication SDK](./SECURITY-AUTH-SDK.md) -Technical security implementation details: -- Authentication flow -- Session management -- Rate limiting -- Security headers - -## Quick Links - -### For Developers -- [Migration Guide](./MIGRATION-GUIDE.md) - Start here if migrating existing code -- [Architecture](./ARCHITECTURE.md) - Understand the system design -- [Troubleshooting](./TROUBLESHOOTING.md) - Common issues and solutions - -### For Administrators -- [Virtual Key Management](./VIRTUAL-KEY-MANAGEMENT.md) - Managing API keys -- [Security Considerations](./SECURITY-CONSIDERATIONS.md) - Security best practices -- [Troubleshooting](./TROUBLESHOOTING.md) - Debugging authentication issues - -### For Security Teams -- [Security Considerations](./SECURITY-CONSIDERATIONS.md) - Security overview -- [Security Authentication SDK](./SECURITY-AUTH-SDK.md) - Technical details -- [Virtual Key Management](./VIRTUAL-KEY-MANAGEMENT.md) - Key security model - -## Getting Started - -1. **New to WebUI?** Start with [Architecture](./ARCHITECTURE.md) -2. **Migrating code?** Read the [Migration Guide](./MIGRATION-GUIDE.md) -3. **Having issues?** Check [Troubleshooting](./TROUBLESHOOTING.md) -4. **Security concerns?** Review [Security Considerations](./SECURITY-CONSIDERATIONS.md) - -## Contributing - -When adding new documentation: -1. Use clear, descriptive filenames -2. Include a table of contents for long documents -3. Add code examples where appropriate -4. Update this index file -5. Test all code examples - -## Additional Resources - -- [WebUI README](../README.md) - Main project documentation -- [Conduit Documentation](https://github.com/knnlabs/Conduit/docs) - Platform documentation -- [SDK Documentation](https://www.npmjs.com/package/@knn_labs/conduit-core-client) - SDK reference \ No newline at end of file diff --git a/ConduitLLM.WebUI/docs/SDK_TYPE_MAPPING.md b/ConduitLLM.WebUI/docs/SDK_TYPE_MAPPING.md deleted file mode 100755 index f4092dd07..000000000 --- a/ConduitLLM.WebUI/docs/SDK_TYPE_MAPPING.md +++ /dev/null @@ -1,116 +0,0 @@ -# SDK to WebUI Type Mappings - -This document tracks the differences between SDK types and WebUI types that need mapping functions. - -## VirtualKey - -| SDK Field (VirtualKeyDto) | WebUI Field (VirtualKey) | Type | Notes | -|---------------------------|--------------------------|------|-------| -| keyName | name | string | Display name | -| apiKey | key | string | API key value | -| maxBudget | budget | number | Budget limit | -| isEnabled | isActive | boolean | Status | -| budgetDuration | budgetPeriod | 'Daily' \| 'Monthly' \| 'Total' | Enum values differ | -| budgetStartDate | (not used) | string | SDK only | -| expiresAt | expirationDate | string \| null | Expiration | -| createdAt | createdDate | string | Creation timestamp | -| updatedAt | modifiedDate | string | Last modified | -| lastUsedAt | lastUsedDate | string \| null | Last usage | -| requestCount | (not used) | number | SDK only | -| rateLimitRpm | (not used) | number | SDK only | -| rateLimitRpd | (not used) | number | SDK only | -| keyPrefix | (not used) | string | SDK only | -| metadata | metadata | string vs Record | Type differs | -| (not present) | allowedProviders | string[] \| null | WebUI only | - -## Provider - -| SDK Field (ProviderCredentialDto) | WebUI Field (Provider) | Type | Notes | -|-----------------------------------|------------------------|------|-------| -| id | id | number vs string | Type differs | -| providerName | name | string | Display name | -| (combined) | type | string | WebUI derives from name | -| isEnabled | isEnabled | boolean | Same | -| apiEndpoint | endpoint | string | API endpoint | -| organizationId | (in configuration) | string | Part of config in WebUI | -| additionalConfig | configuration | string vs Record | Type differs | -| createdAt | createdDate | string | Creation timestamp | -| updatedAt | modifiedDate | string | Last modified | -| (not present) | supportedModels | string[] | WebUI only | - -## ModelMapping - -| SDK Field (ModelProviderMappingDto) | WebUI Field (ModelMapping) | Type | Notes | -|-------------------------------------|---------------------------|------|-------| -| id | id | number vs string | Type differs | -| modelAlias | sourceModel | string | Model identifier | -| targetProvider | targetProvider | string | Same | -| targetModel | targetModel | string | Same | -| isEnabled | isActive | boolean | Status | -| priority | priority | number | Same | -| createdAt | createdDate | string | Creation timestamp | -| updatedAt | modifiedDate | string | Last modified | -| capabilities | (not used) | string | SDK only | -| isDefault | (not used) | boolean | SDK only | -| metadata | metadata | string vs Record | Type differs | - -## ProviderHealth - -| SDK Field (ProviderHealthStatusDto) | WebUI Field (ProviderHealth) | Type | Notes | -|-------------------------------------|------------------------------|------|-------| -| providerId | providerId | number vs string | Type differs | -| providerName | providerName | string | Same | -| status | status | string | Same (but enum values may differ) | -| lastCheckTime | lastChecked | string | Check timestamp | -| responseTimeMs | responseTime | number | Same | -| errorMessage | lastError | string | Error details | -| (not present) | uptime | number | WebUI only | -| (not present) | errorRate | number | WebUI only | -| (not present) | incidents | ProviderIncident[] | WebUI only | - -## SystemHealth - -| SDK Field (HealthStatusDto) | WebUI Field (SystemHealth) | Type | Notes | -|-----------------------------|----------------------------|------|-------| -| isHealthy | status | boolean vs 'healthy'\|'degraded'\|'unhealthy' | Type differs | -| version | version | string | Same | -| timestamp | timestamp | string | Same | -| services | services | Array structure differs | Different shape | -| (not present) | uptime | number | WebUI only | -| (not present) | dependencies | DependencyHealth[] | WebUI only | - -## RequestLog - -| SDK Field (RequestLogDto) | WebUI Field (RequestLog) | Type | Notes | -|---------------------------|--------------------------|------|-------| -| id | id | string | Same | -| timestamp | timestamp | string | Same | -| virtualKeyId | virtualKeyId | number | Same | -| provider | provider | string | Same | -| model | model | string | Same | -| endpoint | endpoint | string | Same | -| method | method | string | Same | -| statusCode | statusCode | number | Same | -| latencyMs | latency | number | Field name differs | -| inputTokens | inputTokens | number | Same | -| outputTokens | outputTokens | number | Same | -| totalCost | cost | number | Field name differs | -| errorMessage | error | string | Field name differs | -| ipAddress | clientIp | string | Field name differs | -| userAgent | userAgent | string | Same | -| (not present) | virtualKeyName | string | WebUI only | - -## Common Type Differences - -1. **ID Types**: SDK uses `number` for IDs, WebUI uses `string` -2. **Metadata**: SDK uses `string`, WebUI uses `Record` -3. **Timestamps**: SDK uses `createdAt/updatedAt`, WebUI uses `createdDate/modifiedDate` -4. **Boolean Names**: SDK uses `isEnabled/isActive`, WebUI varies -5. **Enums**: SDK uses PascalCase ('Daily'), WebUI uses lowercase ('daily') - -## Next Steps - -1. Create mapping functions for each entity type -2. Update all imports to use SDK types -3. Apply mappers at API boundaries -4. Remove duplicate type definitions \ No newline at end of file diff --git a/ConduitLLM.WebUI/package-lock.json b/ConduitLLM.WebUI/package-lock.json deleted file mode 100755 index 8fab64243..000000000 --- a/ConduitLLM.WebUI/package-lock.json +++ /dev/null @@ -1,14376 +0,0 @@ -{ - "name": "conduit-webui", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "conduit-webui", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@clerk/nextjs": "^6.25.4", - "@hello-pangea/dnd": "^18.0.1", - "@knn_labs/conduit-admin-client": "file:../SDKs/Node/Admin", - "@knn_labs/conduit-common": "file:../SDKs/Node/Common", - "@knn_labs/conduit-core-client": "file:../SDKs/Node/Core", - "@mantine/carousel": "^8.1.2", - "@mantine/charts": "^8.1.2", - "@mantine/code-highlight": "^8.1.2", - "@mantine/core": "^8.1.2", - "@mantine/dates": "^8.1.2", - "@mantine/form": "^8.1.2", - "@mantine/hooks": "^8.1.2", - "@mantine/modals": "^8.1.2", - "@mantine/notifications": "^8.1.2", - "@mantine/spotlight": "^8.1.2", - "@microsoft/signalr": "^9.0.6", - "@tabler/icons-react": "^3.34.1", - "@tanstack/react-query": "^5.0.0", - "@types/node": "^24.0.15", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@types/video.js": "^7.3.58", - "@typescript-eslint/eslint-plugin": "^8.35.0", - "@typescript-eslint/parser": "^8.35.0", - "axios": "^1.10.0", - "date-fns": "^4.1.0", - "eslint": "^9.30.0", - "next": "^15.5.2", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-markdown": "^10.1.0", - "react-syntax-highlighter": "^15.6.1", - "typescript": "^5.8.3", - "uuid": "^11.1.0", - "video.js": "^8.23.3", - "zod": "^4.0.5", - "zustand": "^5.0.6" - }, - "devDependencies": { - "@playwright/test": "^1.54.1", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.3.0", - "@types/jest": "^30.0.0", - "@types/react-syntax-highlighter": "^15.5.13", - "@types/uuid": "^10.0.0", - "eslint-config-next": "^15.4.2", - "eslint-plugin-eslint-comments": "^3.2.0", - "husky": "^9.0.11", - "jest": "^30.0.4", - "jest-environment-jsdom": "^30.0.4", - "lint-staged": "^16.1.2", - "playwright": "^1.54.1", - "ts-jest": "^29.4.0", - "ts-node": "^10.9.2" - } - }, - "../SDKs/Node/Admin": { - "name": "@knn_labs/conduit-admin-client", - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@knn_labs/conduit-common": "file:../Common", - "@microsoft/signalr": "^8.0.7", - "zod": "^4.0.5" - }, - "devDependencies": { - "@types/jest": "^30.0.0", - "@types/node": "^24.0.15", - "@types/react": "^19.1.8", - "@typescript-eslint/eslint-plugin": "^8.37.0", - "@typescript-eslint/parser": "^8.37.0", - "eslint": "^9.31.0", - "jest": "^30.0.4", - "prettier": "^3.0.0", - "ts-jest": "^29.1.0", - "ts-node": "^10.9.2", - "tsup": "^8.0.0", - "typescript": "^5.8.3" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "next": ">=13.0.0" - }, - "peerDependenciesMeta": { - "next": { - "optional": true - } - } - }, - "../SDKs/Node/Common": { - "name": "@knn_labs/conduit-common", - "version": "0.2.0", - "license": "MIT", - "dependencies": { - "@microsoft/signalr": "^8.0.7" - }, - "devDependencies": { - "@types/node": "^24.0.15", - "tsup": "^8.1.0", - "typescript": "^5.8.3" - }, - "peerDependencies": { - "typescript": ">=4.5.0" - } - }, - "../SDKs/Node/Core": { - "name": "@knn_labs/conduit-core-client", - "version": "0.2.1", - "license": "MIT", - "dependencies": { - "@knn_labs/conduit-common": "file:../Common", - "@microsoft/signalr": "^8.0.7", - "uuid": "^11.1.0", - "zod": "^4.0.5" - }, - "devDependencies": { - "@types/jest": "^30.0.0", - "@types/node": "^24.0.15", - "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^8.37.0", - "@typescript-eslint/parser": "^8.37.0", - "eslint": "^9.31.0", - "jest": "^30.0.4", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.2", - "tsup": "^8.0.1", - "typescript": "^5.8.3" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", - "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@clerk/backend": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-2.7.0.tgz", - "integrity": "sha512-YrZwRd1X9Am1HGlIx01LoENZ5S7zDBmvHw0srUI5EhNBNY1HCWAd8iBF678o1qV95/sOB27A7BiKJiDIgnTiZg==", - "license": "MIT", - "dependencies": { - "@clerk/shared": "^3.18.1", - "@clerk/types": "^4.74.0", - "cookie": "1.0.2", - "standardwebhooks": "^1.0.0", - "tslib": "2.8.1" - }, - "engines": { - "node": ">=18.17.0" - } - }, - "node_modules/@clerk/clerk-react": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-5.40.0.tgz", - "integrity": "sha512-gRhubPU/U/XEjZv2AujFz5aKKJ5C4ogWxzHILOUjrENJI/EiAyO2B7UtWirW2AC6bHnROoRUQFn0CTi4LVlbig==", - "license": "MIT", - "dependencies": { - "@clerk/shared": "^3.18.1", - "@clerk/types": "^4.74.0", - "tslib": "2.8.1" - }, - "engines": { - "node": ">=18.17.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" - } - }, - "node_modules/@clerk/nextjs": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-6.30.0.tgz", - "integrity": "sha512-rQePjoKWUx7K/oYpnlPB2+kgsJdPJtWbIwA2ePyyqY1pbKTlB/SwhzMA+tT8fe6eRPUI0Q6cgpeq43DEEIcZig==", - "license": "MIT", - "dependencies": { - "@clerk/backend": "^2.7.0", - "@clerk/clerk-react": "^5.40.0", - "@clerk/shared": "^3.18.1", - "@clerk/types": "^4.74.0", - "server-only": "0.0.1", - "tslib": "2.8.1" - }, - "engines": { - "node": ">=18.17.0" - }, - "peerDependencies": { - "next": "^13.5.7 || ^14.2.25 || ^15.2.3", - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" - } - }, - "node_modules/@clerk/shared": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.18.1.tgz", - "integrity": "sha512-l/8+X1gM9+Se87USn55B0ZKeSrFgy0ZYHzAptUc1Fgmfyved90JX7n5QgY66DuqbdM5OfGbRNFiCsI2khAgmrA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@clerk/types": "^4.74.0", - "dequal": "2.0.3", - "glob-to-regexp": "0.4.1", - "js-cookie": "3.0.5", - "std-env": "^3.9.0", - "swr": "2.3.4" - }, - "engines": { - "node": ">=18.17.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/@clerk/types": { - "version": "4.74.0", - "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.74.0.tgz", - "integrity": "sha512-9Wt2dWyCtCp2FngRkbHuQXPMttRjvGb77bmyCdGkPmNqQIZp7m4AZRdPtXhcGgxzaAYJkK6psgolbvuKDk+1jA==", - "license": "MIT", - "dependencies": { - "csstype": "3.1.3" - }, - "engines": { - "node": ">=18.17.0" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", - "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", - "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.0.2", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@emnapi/core": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", - "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.4", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", - "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.15.2", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz", - "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react": { - "version": "0.26.28", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", - "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.1.2", - "@floating-ui/utils": "^0.2.8", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz", - "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.3" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, - "node_modules/@hello-pangea/dnd": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", - "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.26.7", - "css-box-model": "^1.2.1", - "raf-schd": "^4.0.3", - "react-redux": "^9.2.0", - "redux": "^5.0.1" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.4.4" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.5.tgz", - "integrity": "sha512-xY6b0XiL0Nav3ReresUarwl2oIz1gTnxGbGpho9/rbUWsLH0f1OD/VT84xs8c7VmH7MChnLb0pag6PhZhAdDiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.0.5", - "jest-util": "30.0.5", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.5.tgz", - "integrity": "sha512-fKD0OulvRsXF1hmaFgHhVJzczWzA1RXMMo9LTPuFXo9q/alDbME3JIyWYqovWsUBWSoBcsHaGPSLF9rz4l9Qeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.0.5", - "@jest/pattern": "30.0.1", - "@jest/reporters": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-changed-files": "30.0.5", - "jest-config": "30.0.5", - "jest-haste-map": "30.0.5", - "jest-message-util": "30.0.5", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-resolve-dependencies": "30.0.5", - "jest-runner": "30.0.5", - "jest-runtime": "30.0.5", - "jest-snapshot": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "jest-watcher": "30.0.5", - "micromatch": "^4.0.8", - "pretty-format": "30.0.5", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/diff-sequences": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", - "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "jest-mock": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment-jsdom-abstract": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.0.5.tgz", - "integrity": "sha512-gpWwiVxZunkoglP8DCnT3As9x5O8H6gveAOpvaJd2ATAoSh7ZSSCWbr9LQtUMvr8WD3VjG9YnDhsmkCK5WN1rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/fake-timers": "30.0.5", - "@jest/types": "30.0.5", - "@types/jsdom": "^21.1.7", - "@types/node": "*", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/@jest/expect": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.5.tgz", - "integrity": "sha512-6udac8KKrtTtC+AXZ2iUN/R7dp7Ydry+Fo6FPFnDG54wjVMnb6vW/XNlf7Xj8UDjAE3aAVAsR4KFyKk3TCXmTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "30.0.5", - "jest-snapshot": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz", - "integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", - "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@sinonjs/fake-timers": "^13.0.0", - "@types/node": "*", - "jest-message-util": "30.0.5", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/get-type": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", - "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.5.tgz", - "integrity": "sha512-7oEJT19WW4oe6HR7oLRvHxwlJk2gev0U9px3ufs8sX9PoD1Eza68KF0/tlN7X0dq/WVsBScXQGgCldA1V9Y/jA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/expect": "30.0.5", - "@jest/types": "30.0.5", - "jest-mock": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.5.tgz", - "integrity": "sha512-mafft7VBX4jzED1FwGC1o/9QUM2xebzavImZMeqnsklgcyxBto8mV4HzNSzUrryJ+8R9MFOM3HgYuDradWR+4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "@jridgewell/trace-mapping": "^0.3.25", - "@types/node": "*", - "chalk": "^4.1.2", - "collect-v8-coverage": "^1.0.2", - "exit-x": "^0.2.2", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^5.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "30.0.5", - "jest-util": "30.0.5", - "jest-worker": "30.0.5", - "slash": "^3.0.0", - "string-length": "^4.0.2", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/snapshot-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.5.tgz", - "integrity": "sha512-XcCQ5qWHLvi29UUrowgDFvV4t7ETxX91CbDczMnoqXPOIcZOxyNdSjm6kV5XMc8+HkxfRegU/MUmnTbJRzGrUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "natural-compare": "^1.4.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "callsites": "^3.1.0", - "graceful-fs": "^4.2.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.5.tgz", - "integrity": "sha512-wPyztnK0gbDMQAJZ43tdMro+qblDHH1Ru/ylzUo21TBKqt88ZqnKKK2m30LKmLLoKtR2lxdpCC/P3g1vfKcawQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.0.5", - "@jest/types": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "collect-v8-coverage": "^1.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.5.tgz", - "integrity": "sha512-Aea/G1egWoIIozmDD7PBXUOxkekXl7ueGzrsGGi1SbeKgQqCYCIf+wfbflEbf2LiPxL8j2JZGLyrzZagjvW4YQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.0.5", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.5.tgz", - "integrity": "sha512-Vk8amLQCmuZyy6GbBht1Jfo9RSdBtg7Lks+B0PecnjI8J+PCLQPGh7uI8Q/2wwpW2gLdiAfiHNsmekKlywULqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.0.5", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.0", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "jest-regex-util": "30.0.1", - "jest-util": "30.0.5", - "micromatch": "^4.0.8", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/types": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", - "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@knn_labs/conduit-admin-client": { - "resolved": "../SDKs/Node/Admin", - "link": true - }, - "node_modules/@knn_labs/conduit-common": { - "resolved": "../SDKs/Node/Common", - "link": true - }, - "node_modules/@knn_labs/conduit-core-client": { - "resolved": "../SDKs/Node/Core", - "link": true - }, - "node_modules/@mantine/carousel": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@mantine/carousel/-/carousel-8.2.4.tgz", - "integrity": "sha512-uAYm7TNfg6oi8yl3Kbpn4lJPmqgzUsSDBH/v8if7qC1FVrspT3w6wCliVSy13aNTHwCvQwmFx3wX112pEaEmbg==", - "license": "MIT", - "peerDependencies": { - "@mantine/core": "8.2.4", - "@mantine/hooks": "8.2.4", - "embla-carousel": ">=8.0.0", - "embla-carousel-react": ">=8.0.0", - "react": "^18.x || ^19.x", - "react-dom": "^18.x || ^19.x" - } - }, - "node_modules/@mantine/charts": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@mantine/charts/-/charts-8.2.4.tgz", - "integrity": "sha512-fO2M+gNs5Oc1dnfq0GVl51pSVCAVIgWCJbwKPsatZG5LK9TSou+yu/G6iN5/k721OmVyoNJf/G44foDzLUG54A==", - "license": "MIT", - "peerDependencies": { - "@mantine/core": "8.2.4", - "@mantine/hooks": "8.2.4", - "react": "^18.x || ^19.x", - "react-dom": "^18.x || ^19.x", - "recharts": ">=2.13.3" - } - }, - "node_modules/@mantine/code-highlight": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@mantine/code-highlight/-/code-highlight-8.2.4.tgz", - "integrity": "sha512-RBfX+nrLmggPpPf0wVim12/HfO31ufZayOfsraMrWP/hhfI+5sG+QGQU9KMVVIxAE6MtC1vPsC5nRdVxKx1rnw==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1", - "highlight.js": "^11.10.0" - }, - "peerDependencies": { - "@mantine/core": "8.2.4", - "@mantine/hooks": "8.2.4", - "react": "^18.x || ^19.x", - "react-dom": "^18.x || ^19.x" - } - }, - "node_modules/@mantine/core": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.2.4.tgz", - "integrity": "sha512-bZBxt6CFNLleARe5Ni2oYOcuRdi8ZEaxoZBmdc3sQu24tiX61xTKtjOfHaPffNMpqNQoIbgOi955rk3t1KSLig==", - "license": "MIT", - "dependencies": { - "@floating-ui/react": "^0.26.28", - "clsx": "^2.1.1", - "react-number-format": "^5.4.3", - "react-remove-scroll": "^2.6.2", - "react-textarea-autosize": "8.5.9", - "type-fest": "^4.27.0" - }, - "peerDependencies": { - "@mantine/hooks": "8.2.4", - "react": "^18.x || ^19.x", - "react-dom": "^18.x || ^19.x" - } - }, - "node_modules/@mantine/dates": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-8.2.4.tgz", - "integrity": "sha512-i4t1vKGQhqbT8B+/hK2iF0taYRrWKwNEFXoQrwIrau5OE8MZaIBiMVZaO6qYSW0KAvW1ZIt6SF7UpiZTqnytqQ==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1" - }, - "peerDependencies": { - "@mantine/core": "8.2.4", - "@mantine/hooks": "8.2.4", - "dayjs": ">=1.0.0", - "react": "^18.x || ^19.x", - "react-dom": "^18.x || ^19.x" - } - }, - "node_modules/@mantine/form": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@mantine/form/-/form-8.2.4.tgz", - "integrity": "sha512-dr69dYyHStjn6+yBmMCLuIw0SmogtSPUqheA5mIztboswYUX9p+NRMUqfSjd5TZ9o3QEMt9HsQJsPLn67EIvJQ==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "klona": "^2.0.6" - }, - "peerDependencies": { - "react": "^18.x || ^19.x" - } - }, - "node_modules/@mantine/hooks": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.2.4.tgz", - "integrity": "sha512-ZXQ0uk6UtJa6Gl+ZWMBh4wC7UqCAoWWvau1TOoe05sBrkyGcXSVrTfoCVzjEQaB/h8VEOUWLGtJokkiMQKcMzA==", - "license": "MIT", - "peerDependencies": { - "react": "^18.x || ^19.x" - } - }, - "node_modules/@mantine/modals": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-8.2.4.tgz", - "integrity": "sha512-3o1ZnzrU+Rnp6HkJPHbVllVc9HmTVn4s8UyjGunc9IqnId7wU8GDULpWXNXrBgbvWuJzwIOfGYgw5GCefKUHIA==", - "license": "MIT", - "peerDependencies": { - "@mantine/core": "8.2.4", - "@mantine/hooks": "8.2.4", - "react": "^18.x || ^19.x", - "react-dom": "^18.x || ^19.x" - } - }, - "node_modules/@mantine/notifications": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.2.4.tgz", - "integrity": "sha512-CPyYM1Y9oXxlJl5zTJN0mgJGZh8ZrhdIsA4ZktnpmJMKvGHWQdmtzTcPDu4gwzDNdANsN0f9DtMSp68kNiD1xA==", - "license": "MIT", - "dependencies": { - "@mantine/store": "8.2.4", - "react-transition-group": "4.4.5" - }, - "peerDependencies": { - "@mantine/core": "8.2.4", - "@mantine/hooks": "8.2.4", - "react": "^18.x || ^19.x", - "react-dom": "^18.x || ^19.x" - } - }, - "node_modules/@mantine/spotlight": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@mantine/spotlight/-/spotlight-8.2.4.tgz", - "integrity": "sha512-p9olpP5OMtuh9QZ5HLQMgQE6YDJx6it31szt2dncBukim83laDwuKBEUkweBAsZqIUnBY1kaTrRKov1ADCa0Ew==", - "license": "MIT", - "dependencies": { - "@mantine/store": "8.2.4" - }, - "peerDependencies": { - "@mantine/core": "8.2.4", - "@mantine/hooks": "8.2.4", - "react": "^18.x || ^19.x", - "react-dom": "^18.x || ^19.x" - } - }, - "node_modules/@mantine/store": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.2.4.tgz", - "integrity": "sha512-NYbhSy6UkVXsCDDHau+ZmGuuLgQ1laNINhKRHYabRvH5aSuU9drbgIlraNgzoF/+LeoTSQ8LylsdWNQRq0hqqA==", - "license": "MIT", - "peerDependencies": { - "react": "^18.x || ^19.x" - } - }, - "node_modules/@microsoft/signalr": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-9.0.6.tgz", - "integrity": "sha512-DrhgzFWI9JE4RPTsHYRxh4yr+OhnwKz8bnJe7eIi7mLLjqhJpEb62CiUy/YbFvLqLzcGzlzz1QWgVAW0zyipMQ==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "eventsource": "^2.0.2", - "fetch-cookie": "^2.0.3", - "node-fetch": "^2.6.7", - "ws": "^7.5.10" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@next/env": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz", - "integrity": "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.4.6.tgz", - "integrity": "sha512-2NOu3ln+BTcpnbIDuxx6MNq+pRrCyey4WSXGaJIyt0D2TYicHeO9QrUENNjcf673n3B1s7hsiV5xBYRCK1Q8kA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "3.3.1" - } - }, - "node_modules/@next/eslint-plugin-next/node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/@next/eslint-plugin-next/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz", - "integrity": "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz", - "integrity": "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz", - "integrity": "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz", - "integrity": "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz", - "integrity": "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz", - "integrity": "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz", - "integrity": "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz", - "integrity": "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.4.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@playwright/test": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz", - "integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.54.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", - "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", - "license": "MIT", - "peer": true, - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^10.0.3", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", - "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.38", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", - "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@stablelib/base64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", - "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "license": "MIT", - "peer": true - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT", - "peer": true - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tabler/icons": { - "version": "3.34.1", - "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.34.1.tgz", - "integrity": "sha512-9gTnUvd7Fd/DmQgr3MKY+oJLa1RfNsQo8c/ir3TJAWghOuZXodbtbVp0QBY2DxWuuvrSZFys0HEbv1CoiI5y6A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/codecalm" - } - }, - "node_modules/@tabler/icons-react": { - "version": "3.34.1", - "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.34.1.tgz", - "integrity": "sha512-Ld6g0NqOO05kyyHsfU8h787PdHBm7cFmOycQSIrGp45XcXYDuOK2Bs0VC4T2FWSKZ6bx5g04imfzazf/nqtk1A==", - "license": "MIT", - "dependencies": { - "@tabler/icons": "3.34.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/codecalm" - }, - "peerDependencies": { - "react": ">= 16" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.83.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz", - "integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.84.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.2.tgz", - "integrity": "sha512-cZadySzROlD2+o8zIfbD978p0IphuQzRWiiH3I2ugnTmz4jbjc0+TdibpwqxlzynEen8OulgAg+rzdNF37s7XQ==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.83.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.4.tgz", - "integrity": "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "lodash": "^4.17.21", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/react": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", - "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "30.0.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^30.0.0", - "pretty-format": "^30.0.0" - } - }, - "node_modules/@types/jest/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@types/jest/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@types/jest/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jsdom": { - "version": "21.1.7", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", - "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", - "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.10.0" - } - }, - "node_modules/@types/react": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", - "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.1.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", - "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, - "node_modules/@types/react-syntax-highlighter": { - "version": "15.5.13", - "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", - "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" - }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/video.js": { - "version": "7.3.58", - "resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.58.tgz", - "integrity": "sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==", - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", - "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/type-utils": "8.39.0", - "@typescript-eslint/utils": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.39.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", - "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", - "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.0", - "@typescript-eslint/types": "^8.39.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", - "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", - "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", - "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/utils": "8.39.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", - "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", - "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.39.0", - "@typescript-eslint/tsconfig-utils": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", - "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", - "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.39.0", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@videojs/http-streaming": { - "version": "3.17.2", - "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.17.2.tgz", - "integrity": "sha512-VBQ3W4wnKnVKb/limLdtSD2rAd5cmHN70xoMf4OmuDd0t2kfJX04G+sfw6u2j8oOm2BXYM9E1f4acHruqKnM1g==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^4.1.1", - "aes-decrypter": "^4.0.2", - "global": "^4.4.0", - "m3u8-parser": "^7.2.0", - "mpd-parser": "^1.3.1", - "mux.js": "7.1.0", - "video.js": "^7 || ^8" - }, - "engines": { - "node": ">=8", - "npm": ">=5" - }, - "peerDependencies": { - "video.js": "^8.19.0" - } - }, - "node_modules/@videojs/vhs-utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", - "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "global": "^4.4.0" - }, - "engines": { - "node": ">=8", - "npm": ">=5" - } - }, - "node_modules/@videojs/xhr": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz", - "integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.5.5", - "global": "~4.4.0", - "is-function": "^1.0.1" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/aes-decrypter": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz", - "integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^4.1.1", - "global": "^4.4.0", - "pkcs7": "^1.0.4" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/babel-jest": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz", - "integrity": "sha512-mRijnKimhGDMsizTvBTWotwNpzrkHr+VvZUQBof2AufXKB8NXrL1W69TG20EvOz7aevx6FTJIaBuBkYxS8zolg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "30.0.5", - "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.0", - "babel-preset-jest": "30.0.1", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", - "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", - "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "@types/babel__core": "^7.20.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", - "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "30.0.1", - "babel-preset-current-node-syntax": "^1.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.25.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", - "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001733", - "electron-to-chromium": "^1.5.199", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001734", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", - "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ci-info": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", - "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/commander": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", - "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-box-model": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", - "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", - "license": "MIT", - "dependencies": { - "tiny-invariant": "^1.0.6" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cssstyle": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "peer": true, - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "peer": true, - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "peer": true, - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "peer": true, - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "peer": true, - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "peer": true, - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT", - "peer": true - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "license": "MIT", - "peer": true - }, - "node_modules/decode-named-character-reference": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", - "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/dom-walk": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.199", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", - "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/embla-carousel": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", - "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true - }, - "node_modules/embla-carousel-react": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", - "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", - "license": "MIT", - "peer": true, - "dependencies": { - "embla-carousel": "8.6.0", - "embla-carousel-reactive-utils": "8.6.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/embla-carousel-reactive-utils": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", - "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "embla-carousel": "8.6.0" - } - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-toolkit": { - "version": "1.39.9", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.9.tgz", - "integrity": "sha512-9OtbkZmTA2Qc9groyA1PUNeb6knVTkvB2RSdr/LcJXDL8IdEakaxwXLHXa7VX/Wj0GmdMJPR3WhnPGhiP3E+qg==", - "license": "MIT", - "peer": true, - "workspaces": [ - "docs", - "benchmarks" - ] - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", - "@eslint/plugin-kit": "^0.3.5", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-next": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.4.6.tgz", - "integrity": "sha512-4uznvw5DlTTjrZgYZjMciSdDDMO2SWIuQgUNaFyC2O3Zw3Z91XeIejeVa439yRq2CnJb/KEvE4U2AeN/66FpUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@next/eslint-plugin-next": "15.4.6", - "@rushstack/eslint-patch": "^1.10.3", - "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^5.0.0" - }, - "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", - "typescript": ">=3.3.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-eslint-comments": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", - "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5", - "ignore": "^5.0.5" - }, - "engines": { - "node": ">=6.5.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" - } - }, - "node_modules/eslint-plugin-eslint-comments/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint-plugin-eslint-comments/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", - "hasown": "^2.0.2", - "is-core-module": "^2.16.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.1", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/eventsource": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", - "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/exit-x": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", - "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", - "integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "30.0.5", - "@jest/get-type": "30.0.1", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "license": "MIT" - }, - "node_modules/fast-sha256": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", - "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", - "license": "Unlicense" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fault": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", - "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", - "license": "MIT", - "dependencies": { - "format": "^0.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fetch-cookie": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", - "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", - "license": "Unlicense", - "dependencies": { - "set-cookie-parser": "^2.4.8", - "tough-cookie": "^4.0.0" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" - }, - "node_modules/global": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", - "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "license": "MIT", - "dependencies": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "license": "MIT" - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", - "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", - "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", - "license": "MIT", - "dependencies": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^1.0.0", - "hast-util-parse-selector": "^2.0.0", - "property-information": "^5.0.0", - "space-separated-tokens": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript/node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/hastscript/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/hastscript/node_modules/comma-separated-tokens": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", - "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hastscript/node_modules/property-information": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", - "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hastscript/node_modules/space-separated-tokens": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/highlight.js": { - "version": "11.11.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/highlightjs-vue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", - "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", - "license": "CC0-1.0" - }, - "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/html-url-attributes": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", - "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true, - "license": "MIT", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/inline-style-parser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", - "license": "MIT" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.7.1" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-function": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", - "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", - "license": "MIT" - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jest": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.5.tgz", - "integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "30.0.5", - "@jest/types": "30.0.5", - "import-local": "^3.2.0", - "jest-cli": "30.0.5" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", - "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.1.1", - "jest-util": "30.0.5", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.5.tgz", - "integrity": "sha512-h/sjXEs4GS+NFFfqBDYT7y5Msfxh04EwWLhQi0F8kuWpe+J/7tICSlswU8qvBqumR3kFgHbfu7vU6qruWWBPug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/expect": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "co": "^4.6.0", - "dedent": "^1.6.0", - "is-generator-fn": "^2.1.0", - "jest-each": "30.0.5", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", - "jest-runtime": "30.0.5", - "jest-snapshot": "30.0.5", - "jest-util": "30.0.5", - "p-limit": "^3.1.0", - "pretty-format": "30.0.5", - "pure-rand": "^7.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-cli": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.5.tgz", - "integrity": "sha512-Sa45PGMkBZzF94HMrlX4kUyPOwUpdZasaliKN3mifvDmkhLYqLLg8HQTzn6gq7vJGahFYMQjXgyJWfYImKZzOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/types": "30.0.5", - "chalk": "^4.1.2", - "exit-x": "^0.2.2", - "import-local": "^3.2.0", - "jest-config": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "yargs": "^17.7.2" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.5.tgz", - "integrity": "sha512-aIVh+JNOOpzUgzUnPn5FLtyVnqc3TQHVMupYtyeURSb//iLColiMIR8TxCIDKyx9ZgjKnXGucuW68hCxgbrwmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/get-type": "30.0.1", - "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.0.5", - "@jest/types": "30.0.5", - "babel-jest": "30.0.5", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "jest-circus": "30.0.5", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.5", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-runner": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "micromatch": "^4.0.8", - "parse-json": "^5.2.0", - "pretty-format": "30.0.5", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "esbuild-register": ">=3.4.0", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "esbuild-register": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-diff": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", - "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.0.1", - "chalk": "^4.1.2", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-diff/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-docblock": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", - "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.5.tgz", - "integrity": "sha512-dKjRsx1uZ96TVyejD3/aAWcNKy6ajMaN531CwWIsrazIqIoXI9TnnpPlkrEYku/8rkS3dh2rbH+kMOyiEIv0xQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1", - "@jest/types": "30.0.5", - "chalk": "^4.1.2", - "jest-util": "30.0.5", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-environment-jsdom": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.0.5.tgz", - "integrity": "sha512-BmnDEoAH+jEjkPrvE9DTKS2r3jYSJWlN/r46h0/DBUxKrkgt2jAZ5Nj4wXLAcV1KWkRpcFqA5zri9SWzJZ1cCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/environment-jsdom-abstract": "30.0.5", - "@types/jsdom": "^21.1.7", - "@types/node": "*", - "jsdom": "^26.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jest-environment-node": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.5.tgz", - "integrity": "sha512-ppYizXdLMSvciGsRsMEnv/5EFpvOdXBaXRBzFUDPWrsfmog4kYrOGWXarLllz6AXan6ZAA/kYokgDWuos1IKDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/fake-timers": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "jest-mock": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.5.tgz", - "integrity": "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.0.5", - "jest-worker": "30.0.5", - "micromatch": "^4.0.8", - "walker": "^1.0.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" - } - }, - "node_modules/jest-leak-detector": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz", - "integrity": "sha512-3Uxr5uP8jmHMcsOtYMRB/zf1gXN3yUIc+iPorhNETG54gErFIiUhLvyY/OggYpSMOEYqsmRxmuU4ZOoX5jpRFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-leak-detector/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-leak-detector/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-leak-detector/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-matcher-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", - "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1", - "chalk": "^4.1.2", - "jest-diff": "30.0.5", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-message-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", - "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.0.5", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.0.5", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-message-util/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-mock": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", - "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.5.tgz", - "integrity": "sha512-d+DjBQ1tIhdz91B79mywH5yYu76bZuE96sSbxj8MkjWVx5WNdt1deEFRONVL4UkKLSrAbMkdhb24XN691yDRHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "slash": "^3.0.0", - "unrs-resolver": "^1.7.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.5.tgz", - "integrity": "sha512-/xMvBR4MpwkrHW4ikZIWRttBBRZgWK4d6xt3xW1iRDSKt4tXzYkMkyPfBnSCgv96cpkrctfXs6gexeqMYqdEpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "30.0.1", - "jest-snapshot": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.5.tgz", - "integrity": "sha512-JcCOucZmgp+YuGgLAXHNy7ualBx4wYSgJVWrYMRBnb79j9PD0Jxh0EHvR5Cx/r0Ce+ZBC4hCdz2AzFFLl9hCiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.0.5", - "@jest/environment": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.5", - "jest-haste-map": "30.0.5", - "jest-leak-detector": "30.0.5", - "jest-message-util": "30.0.5", - "jest-resolve": "30.0.5", - "jest-runtime": "30.0.5", - "jest-util": "30.0.5", - "jest-watcher": "30.0.5", - "jest-worker": "30.0.5", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.5.tgz", - "integrity": "sha512-7oySNDkqpe4xpX5PPiJTe5vEa+Ak/NnNz2bGYZrA1ftG3RL3EFlHaUkA1Cjx+R8IhK0Vg43RML5mJedGTPNz3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/fake-timers": "30.0.5", - "@jest/globals": "30.0.5", - "@jest/source-map": "30.0.1", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "cjs-module-lexer": "^2.1.0", - "collect-v8-coverage": "^1.0.2", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "jest-message-util": "30.0.5", - "jest-mock": "30.0.5", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-snapshot": "30.0.5", - "jest-util": "30.0.5", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.5.tgz", - "integrity": "sha512-T00dWU/Ek3LqTp4+DcW6PraVxjk28WY5Ua/s+3zUKSERZSNyxTqhDXCWKG5p2HAJ+crVQ3WJ2P9YVHpj1tkW+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@babel/generator": "^7.27.5", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1", - "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.0.5", - "@jest/get-type": "30.0.1", - "@jest/snapshot-utils": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "babel-preset-current-node-syntax": "^1.1.0", - "chalk": "^4.1.2", - "expect": "30.0.5", - "graceful-fs": "^4.2.11", - "jest-diff": "30.0.5", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", - "jest-util": "30.0.5", - "pretty-format": "30.0.5", - "semver": "^7.7.2", - "synckit": "^0.11.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", - "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/jest-validate": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.5.tgz", - "integrity": "sha512-ouTm6VFHaS2boyl+k4u+Qip4TSH7Uld5tyD8psQ8abGgt2uYYB8VwVfAHWHjHc0NWmGGbwO5h0sCPOGHHevefw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1", - "@jest/types": "30.0.5", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-validate/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-validate/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-validate/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-watcher": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.5.tgz", - "integrity": "sha512-z9slj/0vOwBDBjN3L4z4ZYaA+pG56d6p3kTUhFRYGvXbXMWhXmb/FIxREZCD06DYUwDKKnj2T80+Pb71CQ0KEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "jest-util": "30.0.5", - "string-length": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-worker": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", - "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.0.5", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/jsdom/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", - "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lint-staged": { - "version": "16.1.5", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.5.tgz", - "integrity": "sha512-uAeQQwByI6dfV7wpt/gVqg+jAPaSp8WwOA8kKC/dv1qw14oGpnpAisY65ibGHUGDUv0rYaZ8CAJZ/1U8hUvC2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.5.0", - "commander": "^14.0.0", - "debug": "^4.4.1", - "lilconfig": "^3.1.3", - "listr2": "^9.0.1", - "micromatch": "^4.0.8", - "nano-spawn": "^1.0.2", - "pidtree": "^0.6.0", - "string-argv": "^0.3.2", - "yaml": "^2.8.1" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=20.17" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/lint-staged/node_modules/chalk": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz", - "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/listr2": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz", - "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^4.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "license": "MIT" - }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lowlight": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", - "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", - "license": "MIT", - "dependencies": { - "fault": "^1.0.0", - "highlight.js": "~10.7.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lowlight/node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/m3u8-parser": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz", - "integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^4.1.1", - "global": "^4.4.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", - "dependencies": { - "dom-walk": "^0.1.0" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mpd-parser": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz", - "integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^4.0.0", - "@xmldom/xmldom": "^0.8.3", - "global": "^4.4.0" - }, - "bin": { - "mpd-to-m3u8-json": "bin/parse.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/mux.js": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz", - "integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.11.2", - "global": "^4.4.0" - }, - "bin": { - "muxjs-transmux": "bin/transmux.js" - }, - "engines": { - "node": ">=8", - "npm": ">=5" - } - }, - "node_modules/nano-spawn": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", - "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/napi-postinstall": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", - "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", - "dev": true, - "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "license": "MIT" - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/next": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", - "integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==", - "license": "MIT", - "dependencies": { - "@next/env": "15.5.2", - "@swc/helpers": "0.5.15", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.2", - "@next/swc-darwin-x64": "15.5.2", - "@next/swc-linux-arm64-gnu": "15.5.2", - "@next/swc-linux-arm64-musl": "15.5.2", - "@next/swc-linux-x64-gnu": "15.5.2", - "@next/swc-linux-x64-musl": "15.5.2", - "@next/swc-win32-arm64-msvc": "15.5.2", - "@next/swc-win32-x64-msvc": "15.5.2", - "sharp": "^0.34.3" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nwsapi": { - "version": "2.2.21", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", - "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkcs7": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", - "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.5.5" - }, - "bin": { - "pkcs7": "bin/cli.js" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/playwright": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz", - "integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.54.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz", - "integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/prismjs": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", - "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/raf-schd": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", - "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", - "license": "MIT" - }, - "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.1" - } - }, - "node_modules/react-is": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", - "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", - "license": "MIT", - "peer": true - }, - "node_modules/react-markdown": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", - "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "html-url-attributes": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "unified": "^11.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=18", - "react": ">=18" - } - }, - "node_modules/react-number-format": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz", - "integrity": "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==", - "license": "MIT", - "peerDependencies": { - "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-redux": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", - "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", - "license": "MIT", - "dependencies": { - "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25 || ^19", - "react": "^18.0 || ^19", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", - "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-syntax-highlighter": { - "version": "15.6.1", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", - "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.3.1", - "highlight.js": "^10.4.1", - "highlightjs-vue": "^1.0.0", - "lowlight": "^1.17.0", - "prismjs": "^1.27.0", - "refractor": "^3.6.0" - }, - "peerDependencies": { - "react": ">= 0.14.0" - } - }, - "node_modules/react-syntax-highlighter/node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/react-textarea-autosize": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", - "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.13", - "use-composed-ref": "^1.3.0", - "use-latest": "^1.2.1" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/recharts": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.1.2.tgz", - "integrity": "sha512-vhNbYwaxNbk/IATK0Ki29k3qvTkGqwvCgyQAQ9MavvvBwjvKnMTswdbklJpcOAoMPN/qxF3Lyqob0zO+ZXkZ4g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@reduxjs/toolkit": "1.x.x || 2.x.x", - "clsx": "^2.1.1", - "decimal.js-light": "^2.5.1", - "es-toolkit": "^1.39.3", - "eventemitter3": "^5.0.1", - "immer": "^10.1.1", - "react-redux": "8.x.x || 9.x.x", - "reselect": "5.1.1", - "tiny-invariant": "^1.3.3", - "use-sync-external-store": "^1.2.2", - "victory-vendor": "^37.0.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "redux": "^5.0.0" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/refractor": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", - "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", - "license": "MIT", - "dependencies": { - "hastscript": "^6.0.0", - "parse-entities": "^2.0.0", - "prismjs": "~1.27.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "license": "MIT", - "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/prismjs": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", - "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "license": "MIT" - }, - "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", - "license": "MIT", - "peer": true - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/server-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", - "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", - "license": "MIT" - }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/stable-hash": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/standardwebhooks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", - "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", - "license": "MIT", - "dependencies": { - "@stablelib/base64": "^1.0.0", - "fast-sha256": "^1.3.0" - } - }, - "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", - "license": "MIT" - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-to-js": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz", - "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.9" - } - }, - "node_modules/style-to-object": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", - "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.4" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swr": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.4.tgz", - "integrity": "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/synckit": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, - "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", - "license": "MIT" - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.86" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-jest": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", - "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bs-logger": "^0.2.6", - "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.8", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.7.2", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0 || ^30.0.0", - "@jest/types": "^29.0.0 || ^30.0.0", - "babel-jest": "^29.0.0 || ^30.0.0", - "jest": "^29.0.0 || ^30.0.0", - "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <6" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jest-util": { - "optional": true - } - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/tsconfig-paths/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "license": "MIT" - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.0" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-composed-ref": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", - "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-isomorphic-layout-effect": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", - "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-latest": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", - "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", - "license": "MIT", - "dependencies": { - "use-isomorphic-layout-effect": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/victory-vendor": { - "version": "37.3.6", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", - "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", - "license": "MIT AND ISC", - "peer": true, - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, - "node_modules/video.js": { - "version": "8.23.4", - "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.23.4.tgz", - "integrity": "sha512-qI0VTlYmKzEqRsz1Nppdfcaww4RSxZAq77z2oNSl3cNg2h6do5C8Ffl0KqWQ1OpD8desWXsCrde7tKJ9gGTEyQ==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@videojs/http-streaming": "^3.17.2", - "@videojs/vhs-utils": "^4.1.1", - "@videojs/xhr": "2.7.0", - "aes-decrypter": "^4.0.2", - "global": "4.4.0", - "m3u8-parser": "^7.2.0", - "mpd-parser": "^1.3.1", - "mux.js": "^7.0.1", - "videojs-contrib-quality-levels": "4.1.0", - "videojs-font": "4.2.0", - "videojs-vtt.js": "0.15.5" - } - }, - "node_modules/videojs-contrib-quality-levels": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz", - "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==", - "license": "Apache-2.0", - "dependencies": { - "global": "^4.4.0" - }, - "engines": { - "node": ">=16", - "npm": ">=8" - }, - "peerDependencies": { - "video.js": "^8" - } - }, - "node_modules/videojs-font": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz", - "integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==", - "license": "Apache-2.0" - }, - "node_modules/videojs-vtt.js": { - "version": "0.15.5", - "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz", - "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==", - "license": "Apache-2.0", - "dependencies": { - "global": "^4.3.1" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz", - "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zustand": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.7.tgz", - "integrity": "sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/ConduitLLM.WebUI/package.json b/ConduitLLM.WebUI/package.json deleted file mode 100755 index 3a01a6e11..000000000 --- a/ConduitLLM.WebUI/package.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "name": "conduit-webui", - "version": "1.0.0", - "description": "Next.js WebUI for Conduit LLM Platform", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "type-check": "tsc --noEmit", - "lint:fix": "next lint --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "prepare": "cd .. && husky install ConduitLLM.WebUI/.husky", - "pre-commit": "lint-staged" - }, - "dependencies": { - "@clerk/nextjs": "^6.25.4", - "@hello-pangea/dnd": "^18.0.1", - "@knn_labs/conduit-admin-client": "file:../SDKs/Node/Admin", - "@knn_labs/conduit-common": "file:../SDKs/Node/Common", - "@knn_labs/conduit-core-client": "file:../SDKs/Node/Core", - "@mantine/carousel": "^8.1.2", - "@mantine/charts": "^8.1.2", - "@mantine/code-highlight": "^8.1.2", - "@mantine/core": "^8.1.2", - "@mantine/dates": "^8.1.2", - "@mantine/form": "^8.1.2", - "@mantine/hooks": "^8.1.2", - "@mantine/modals": "^8.1.2", - "@mantine/notifications": "^8.1.2", - "@mantine/spotlight": "^8.1.2", - "@microsoft/signalr": "^9.0.6", - "@tabler/icons-react": "^3.34.1", - "@tanstack/react-query": "^5.0.0", - "@types/node": "^24.0.15", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@types/video.js": "^7.3.58", - "@typescript-eslint/eslint-plugin": "^8.35.0", - "@typescript-eslint/parser": "^8.35.0", - "axios": "^1.10.0", - "date-fns": "^4.1.0", - "eslint": "^9.30.0", - "next": "^15.5.2", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-markdown": "^10.1.0", - "react-syntax-highlighter": "^15.6.1", - "typescript": "^5.8.3", - "uuid": "^11.1.0", - "video.js": "^8.23.3", - "zod": "^4.0.5", - "zustand": "^5.0.6" - }, - "keywords": [ - "conduit", - "llm", - "nextjs", - "react", - "typescript", - "mantine" - ], - "author": "KNN Labs", - "license": "ISC", - "devDependencies": { - "@playwright/test": "^1.54.1", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.3.0", - "@types/jest": "^30.0.0", - "@types/react-syntax-highlighter": "^15.5.13", - "@types/uuid": "^10.0.0", - "eslint-config-next": "^15.4.2", - "eslint-plugin-eslint-comments": "^3.2.0", - "husky": "^9.0.11", - "jest": "^30.0.4", - "jest-environment-jsdom": "^30.0.4", - "lint-staged": "^16.1.2", - "playwright": "^1.54.1", - "ts-jest": "^29.4.0", - "ts-node": "^10.9.2" - }, - "lint-staged": { - "*.{ts,tsx}": [ - "eslint --fix", - "tsc --noEmit" - ], - "*.{js,jsx}": [ - "eslint --fix" - ], - "*.{json,md}": [ - "prettier --write" - ] - } -} diff --git a/ConduitLLM.WebUI/src/app/access-denied/page.tsx b/ConduitLLM.WebUI/src/app/access-denied/page.tsx deleted file mode 100755 index c82ed269d..000000000 --- a/ConduitLLM.WebUI/src/app/access-denied/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Access Denied Page - * - * This page is shown when a user without admin privileges attempts to access the admin WebUI. - * - * Feature: External Redirect - * If the ACCESS_DENIED_REDIRECT environment variable is set, users will be automatically - * redirected to the specified URL instead of seeing this access denied page. - * - * Configuration: - * - Set ACCESS_DENIED_REDIRECT in your environment with a valid URL - * - Example: ACCESS_DENIED_REDIRECT="https://your-main-site.com" - * - Can be configured in docker-compose.yml or .env files - */ - -'use client'; - -import { Container, Title, Text, Button, Group, Paper } from '@mantine/core'; -import { IconLock } from '@tabler/icons-react'; -import { SignOutButton } from '@clerk/nextjs'; -import { useAuth } from '@/contexts/AuthContext'; -import { useRouter } from 'next/navigation'; - -export default function AccessDeniedPage() { - const { isAuthDisabled } = useAuth(); - const router = useRouter(); - - return ( - - - - - - - - Access Denied - - - - You don't have permission to access this application. - Only administrators with proper authorization can use the Conduit WebUI. - - - - If you believe this is an error, please contact your system administrator. - - - - {isAuthDisabled ? ( - - ) : ( - - - - )} - - - - ); -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/api/auth/client-config/route.ts b/ConduitLLM.WebUI/src/app/api/auth/client-config/route.ts deleted file mode 100644 index ebdbf3226..000000000 --- a/ConduitLLM.WebUI/src/app/api/auth/client-config/route.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { NextResponse } from 'next/server'; -import { auth } from '@clerk/nextjs/server'; - -/** - * GET /api/auth/client-config - * Returns client configuration for authenticated users - * This endpoint provides the necessary configuration for client-side SDK usage - */ -export async function GET() { - try { - // Verify user is authenticated - const { userId } = await auth(); - if (!userId) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - // In production, you might want to create user-specific virtual keys - // For now, we'll use the WebUI virtual key from environment - const apiKey = process.env.NEXT_PUBLIC_CONDUIT_API_KEY ?? process.env.CONDUIT_API_TO_API_BACKEND_AUTH_KEY; - // Use CONDUIT_API_EXTERNAL_URL for browser-accessible API endpoints - const baseURL = process.env.CONDUIT_API_EXTERNAL_URL ?? 'http://localhost:5000'; - - if (!apiKey) { - console.error('No API key available for client configuration'); - return NextResponse.json( - { error: 'Configuration error' }, - { status: 500 } - ); - } - - // Return client configuration - return NextResponse.json({ - apiKey, - baseURL, - // Include other non-sensitive configuration if needed - features: { - signalR: true, - videoProgressTracking: true, - } - }); - } catch (error) { - console.error('Error in client config endpoint:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/api/auth/ephemeral-key/route.ts b/ConduitLLM.WebUI/src/app/api/auth/ephemeral-key/route.ts deleted file mode 100644 index e297f20e8..000000000 --- a/ConduitLLM.WebUI/src/app/api/auth/ephemeral-key/route.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { handleSDKError } from '@/lib/errors/sdk-errors'; -import { getServerAdminClient, getServerCoreClient } from '@/lib/server/sdk-config'; - -interface EphemeralKeyRequest { - purpose?: string; // Optional purpose for logging/tracking -} - -interface EphemeralKeyResponse { - ephemeralKey: string; - expiresAt: string; - expiresInSeconds: number; - coreApiUrl: string; // Include the Core API URL for direct connection -} - -// POST /api/auth/ephemeral-key - Generate an ephemeral key for direct API access -export async function POST(request: NextRequest) { - try { - const body = await request.json() as EphemeralKeyRequest; - - // Get the WebUI's virtual key from Admin API - const adminClient = getServerAdminClient(); - let webuiVirtualKey: string; - - try { - webuiVirtualKey = await adminClient.system.getWebUIVirtualKey(); - } catch (error) { - console.error('Failed to get WebUI virtual key:', error); - return NextResponse.json( - { error: 'Failed to get WebUI virtual key' }, - { status: 500 } - ); - } - - // Get request metadata for tracking - const sourceIP = request.headers.get('x-forwarded-for') ?? - request.headers.get('x-real-ip') ?? - 'unknown'; - const userAgent = request.headers.get('user-agent') ?? 'unknown'; - - // Use Core SDK to generate ephemeral key - const coreClient = await getServerCoreClient(); - const response = await coreClient.auth.generateEphemeralKey(webuiVirtualKey, { - metadata: { - sourceIP, - userAgent, - purpose: body.purpose ?? 'web-ui-request' - } - }); - - // Return the ephemeral key with Core API URL - // Use the external URL that the browser can access - const result: EphemeralKeyResponse = { - ...response, - coreApiUrl: process.env.CONDUIT_API_EXTERNAL_URL ?? 'http://localhost:5000', - }; - - return NextResponse.json(result); - } catch (error) { - console.error('Error generating ephemeral key:', error); - return handleSDKError(error); - } -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/api/auth/ephemeral-master-key/route.ts b/ConduitLLM.WebUI/src/app/api/auth/ephemeral-master-key/route.ts deleted file mode 100644 index f2a9208f1..000000000 --- a/ConduitLLM.WebUI/src/app/api/auth/ephemeral-master-key/route.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { handleSDKError } from '@/lib/errors/sdk-errors'; - -interface EphemeralMasterKeyRequest { - purpose?: string; // Optional purpose for logging/tracking -} - -interface EphemeralMasterKeyResponse { - ephemeralMasterKey: string; - expiresAt: string; - expiresInSeconds: number; - adminApiUrl: string; // Include the Admin API URL for direct connection -} - -// POST /api/auth/ephemeral-master-key - Generate an ephemeral master key for direct API access -export async function POST(request: NextRequest) { - try { - const body = await request.json() as EphemeralMasterKeyRequest; - - // Get master key from environment - const masterKey = process.env.CONDUIT_API_TO_API_BACKEND_AUTH_KEY; - if (!masterKey) { - console.error('Master key not configured'); - return NextResponse.json( - { error: 'Master key not configured' }, - { status: 500 } - ); - } - - // Get request metadata for tracking - const sourceIP = request.headers.get('x-forwarded-for') ?? - request.headers.get('x-real-ip') ?? - 'unknown'; - const userAgent = request.headers.get('user-agent') ?? 'unknown'; - - // In development mode with DISABLE_CLERK_AUTH=true, - // we return the master key directly without calling the Admin API - // In production, this would call the Admin API to generate a real ephemeral key - - const isDevelopment = process.env.DISABLE_CLERK_AUTH === 'true'; - - if (isDevelopment) { - // Development mode: return the master key directly - const result: EphemeralMasterKeyResponse = { - ephemeralMasterKey: masterKey, - expiresAt: new Date(Date.now() + 3600000).toISOString(), // 1 hour from now - expiresInSeconds: 3600, - adminApiUrl: process.env.CONDUIT_ADMIN_API_EXTERNAL_URL ?? 'http://localhost:5002', - }; - - return NextResponse.json(result); - } - - // Production mode: call the Admin API's ephemeral master key endpoint - const adminApiUrl = process.env.CONDUIT_ADMIN_API_BASE_URL ?? 'http://admin-api:5002'; - const masterKeyHeader = 'X-Master-Key'; - const ephemeralKeyResponse = await fetch(`${adminApiUrl}/api/admin/auth/ephemeral-master-key`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - [masterKeyHeader]: masterKey, - }, - body: JSON.stringify({ - metadata: { - sourceIP, - userAgent, - purpose: body.purpose ?? 'web-ui-request' - } - }), - }); - - if (!ephemeralKeyResponse.ok) { - const errorText = await ephemeralKeyResponse.text(); - console.error('Failed to generate ephemeral master key:', errorText); - throw new Error(`Failed to generate ephemeral master key: ${ephemeralKeyResponse.status}`); - } - - const response = await ephemeralKeyResponse.json() as EphemeralMasterKeyResponse; - - // Return the ephemeral master key with Admin API URL - // Use the external URL that the browser can access - const result: EphemeralMasterKeyResponse = { - ...response, - adminApiUrl: process.env.CONDUIT_ADMIN_API_EXTERNAL_URL ?? 'http://localhost:5002', - }; - - return NextResponse.json(result); - } catch (error) { - console.error('Error generating ephemeral master key:', error); - return handleSDKError(error); - } -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/api/chat/completions/route.ts b/ConduitLLM.WebUI/src/app/api/chat/completions/route.ts deleted file mode 100644 index 2110879a9..000000000 --- a/ConduitLLM.WebUI/src/app/api/chat/completions/route.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { NextRequest } from 'next/server'; -import { handleSDKError } from '@/lib/errors/sdk-errors'; -import { getServerCoreClient } from '@/lib/server/sdk-config'; -import type { ChatCompletionRequest } from '@knn_labs/conduit-core-client'; - -// POST /api/chat/completions - Create chat completions using Core SDK -export async function POST(request: NextRequest) { - try { - const body = await request.json() as ChatCompletionRequest; - const coreClient = await getServerCoreClient(); - - // Handle both streaming and non-streaming requests - if (body.stream) { - // For streaming, we need to return a proper SSE response - // The SDK returns an async iterable for streaming - const stream = await coreClient.chat.create({ - ...body, - stream: true - }); - - // Create a TransformStream to convert SDK chunks to SSE format - const encoder = new TextEncoder(); - const transformStream = new TransformStream({ - async start(controller) { - try { - for await (const chunk of stream) { - // Format as Server-Sent Events - const sseData = `data: ${JSON.stringify(chunk)}\n\n`; - controller.enqueue(encoder.encode(sseData)); - } - // Send the final [DONE] message - controller.enqueue(encoder.encode('data: [DONE]\n\n')); - controller.terminate(); - } catch (error) { - // Send error as SSE event - const errorData = { - error: { - message: error instanceof Error ? error.message : 'Stream error', - type: 'stream_error' - } - }; - controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorData)}\n\n`)); - controller.terminate(); - } - } - }); - - // Return streaming response with proper headers - const headers = new Headers(); - headers.set('Content-Type', 'text/event-stream'); - headers.set('Cache-Control', 'no-cache'); - headers.set('Connection', 'keep-alive'); - - return new Response(transformStream.readable, { headers }); - } else { - // Non-streaming request - const result = await coreClient.chat.create({ - ...body, - stream: false - }); - - return Response.json(result); - } - } catch (error) { - // For non-streaming errors, use the standard error handler - return handleSDKError(error); - } -} - -// OPTIONS for CORS if needed -export async function OPTIONS() { - const headers = new Headers(); - headers.set('Access-Control-Allow-Origin', '*'); - headers.set('Access-Control-Allow-Methods', 'POST, OPTIONS'); - headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - - return new Response(null, { - status: 200, - headers, - }); -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/api/discovery/models/route.ts b/ConduitLLM.WebUI/src/app/api/discovery/models/route.ts deleted file mode 100644 index bee42488e..000000000 --- a/ConduitLLM.WebUI/src/app/api/discovery/models/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getServerCoreClient } from '@/lib/server/coreClient'; - -export async function GET(request: NextRequest) { - try { - const searchParams = request.nextUrl.searchParams; - const capability = searchParams.get('capability'); - - // Get Core client with proper authentication - const coreClient = await getServerCoreClient(); - - // Use the discovery service to get models - const response = await coreClient.discovery.getModels(); - - // Filter by capability if provided - let models = response.data; - if (capability) { - models = models.filter(model => { - const capabilityKey = capability.replace('-', '_').toLowerCase(); - const capabilities = model.capabilities; - if (!capabilities) return false; - - // Type-safe capability checking - return capabilityKey in capabilities && - capabilities[capabilityKey as keyof typeof capabilities] === true; - }); - } - - return NextResponse.json({ - data: models, - count: models.length - }); - } catch (error) { - console.error('Error fetching discovery models:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/api/health/route.ts b/ConduitLLM.WebUI/src/app/api/health/route.ts deleted file mode 100755 index 7399200a9..000000000 --- a/ConduitLLM.WebUI/src/app/api/health/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NextResponse } from 'next/server'; - -export async function GET() { - return NextResponse.json( - { - status: 'healthy', - service: 'ConduitLLM.WebUI', - timestamp: new Date().toISOString() - }, - { status: 200 } - ); -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/api/images/generate/route.ts b/ConduitLLM.WebUI/src/app/api/images/generate/route.ts deleted file mode 100644 index c5dd70a33..000000000 --- a/ConduitLLM.WebUI/src/app/api/images/generate/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { handleSDKError } from '@/lib/errors/sdk-errors'; -import { getServerCoreClient } from '@/lib/server/sdk-config'; -import type { ImageGenerationRequest } from '@knn_labs/conduit-core-client'; - -// POST /api/images/generate - Generate images using Core SDK -export async function POST(request: NextRequest) { - try { - const body = await request.json() as ImageGenerationRequest; - const coreClient = await getServerCoreClient(); - - // Use the SDK's image generation method - const result = await coreClient.images.generate(body); - - return NextResponse.json(result); - } catch (error) { - return handleSDKError(error); - } -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/api/videos/generate/route.ts b/ConduitLLM.WebUI/src/app/api/videos/generate/route.ts deleted file mode 100755 index b5aee6408..000000000 --- a/ConduitLLM.WebUI/src/app/api/videos/generate/route.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { handleSDKError } from '@/lib/errors/sdk-errors'; -import { getServerCoreClient } from '@/lib/server/sdk-config'; -import type { AsyncVideoGenerationRequest } from '@/app/videos/types'; - -// POST /api/videos/generate - Generate videos using Core SDK -export async function POST(request: NextRequest) { - try { - const body = await request.json() as AsyncVideoGenerationRequest & { useProgressTracking?: boolean }; - const coreClient = await getServerCoreClient(); - - // Check if client wants to use the new progress tracking method - // Note: Server-side SDK doesn't have SignalR, so we still use generateAsync - // The client will need to establish its own SignalR connection for real-time updates - const result = await coreClient.videos.generateAsync(body); - - // Log the actual response structure for debugging - console.warn('Video generation API response:', JSON.stringify(result, null, 2)); - - // Define proper types for both PascalCase (from Core API) and snake_case (expected by SDK) - interface CoreApiResponse { - TaskId?: string; - taskId?: string; // camelCase variant - Status?: string; - status?: string; - CreatedAt?: string; - createdAt?: string; // camelCase variant - EstimatedCompletionTime?: number; - estimatedCompletionTime?: number; // camelCase variant - // Snake case variants that SDK might return - task_id?: string; - created_at?: string; - estimated_time_to_completion?: number; - message?: string; - checkStatusUrl?: string; - } - - // Cast to our known response type - const typedResult = result as unknown as CoreApiResponse; - - // The Core API returns various casing formats depending on the SDK version - // We need to handle all cases: PascalCase, camelCase, and snake_case - const taskId = typedResult.task_id ?? typedResult.taskId ?? typedResult.TaskId; - const status = typedResult.status ?? typedResult.Status ?? 'pending'; - const createdAt = typedResult.created_at ?? typedResult.createdAt ?? typedResult.CreatedAt; - - if (!taskId) { - console.error('Invalid response from Core API, missing task_id:', result); - throw new Error('Invalid response from video generation API - no task ID'); - } - - // Return a normalized response with snake_case fields - return NextResponse.json({ - task_id: taskId, - status: status, - created_at: createdAt, - message: typedResult.message ?? 'Video generation started', - estimated_time_to_completion: typedResult.estimated_time_to_completion ?? typedResult.estimatedCompletionTime ?? typedResult.EstimatedCompletionTime, - // SignalR token removed - clients will use ephemeral keys - // Add a flag to indicate that client-side progress tracking is available - supportsProgressTracking: body.useProgressTracking ?? false - }); - } catch (error) { - return handleSDKError(error); - } -} diff --git a/ConduitLLM.WebUI/src/app/api/videos/tasks/[taskId]/route.ts b/ConduitLLM.WebUI/src/app/api/videos/tasks/[taskId]/route.ts deleted file mode 100755 index abc528fac..000000000 --- a/ConduitLLM.WebUI/src/app/api/videos/tasks/[taskId]/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { handleSDKError } from '@/lib/errors/sdk-errors'; -import { getServerCoreClient } from '@/lib/server/sdk-config'; -// Types are inferred from the SDK methods - -// GET /api/videos/tasks/[taskId] - Get video generation task status -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ taskId: string }> } -) { - - try { - const { taskId } = await params; - const coreClient = await getServerCoreClient(); - - // Get task status from Core SDK - const status = await coreClient.videos.getTaskStatus(taskId); - - return NextResponse.json(status); - } catch (error) { - return handleSDKError(error); - } -} - -// DELETE /api/videos/tasks/[taskId] - Cancel video generation task -export async function DELETE( - request: NextRequest, - { params }: { params: Promise<{ taskId: string }> } -) { - - try { - const { taskId } = await params; - const coreClient = await getServerCoreClient(); - - // Cancel task via Core SDK - await coreClient.videos.cancelTask(taskId); - - return NextResponse.json({ success: true, message: 'Task cancelled successfully' }); - } catch (error) { - return handleSDKError(error); - } -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/chat/components/ChatInterface.tsx b/ConduitLLM.WebUI/src/app/chat/components/ChatInterface.tsx deleted file mode 100755 index f2160903c..000000000 --- a/ConduitLLM.WebUI/src/app/chat/components/ChatInterface.tsx +++ /dev/null @@ -1,241 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { - Container, - Paper, - Stack, - Center, - Loader, - Alert, - Group, - Badge, - Collapse, - ActionIcon -} from '@mantine/core'; -import { IconAlertCircle, IconSettings, IconChevronUp } from '@tabler/icons-react'; -import { ModelSelector } from './ModelSelector'; -import { ChatInput } from './ChatInput'; -import { ChatMessages } from './ChatMessages'; -import { ChatSettings } from './ChatSettings'; -import { TokenCounter } from './TokenCounter'; -import { ErrorDisplay } from '@/components/common/ErrorDisplay'; -import { - ChatMessage, -} from '../types'; -import { usePerformanceSettings } from '../hooks/usePerformanceSettings'; -import { useChatStore } from '../hooks/useChatStore'; -import { useModels } from '../hooks/useModels'; -import { useDiscoveryModels } from '../hooks/useDiscoveryModels'; -import { useChatStreamingLogic } from './ChatStreamingLogic'; -import { DynamicParameters } from '@/components/parameters/DynamicParameters'; -import { useParameterState } from '@/components/parameters/hooks/useParameterState'; -import Link from 'next/link'; - -export function ChatInterface() { - const { data: modelData, isLoading: modelsLoading } = useModels(); - const { data: discoveryData } = useDiscoveryModels(); // Remove 'chat' filter since SupportsChat is false in DB - const [selectedModel, setSelectedModel] = useState(null); - const [messages, setMessages] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [streamingContent, setStreamingContent] = useState(''); - const [tokensPerSecond, setTokensPerSecond] = useState(null); - const [showSettings, setShowSettings] = useState(false); - const [showParameters] = useState(false); - - const performanceSettings = usePerformanceSettings(); - const { - getActiveSession, - createSession, - activeSessionId - } = useChatStore(); - - // Set initial model when data loads - useEffect(() => { - if (modelData && modelData.length > 0 && !selectedModel) { - setSelectedModel(modelData[0].id); - } - }, [modelData, selectedModel]); - - // Ensure we have an active session - useEffect(() => { - if (selectedModel && !activeSessionId) { - createSession(selectedModel); - } - }, [selectedModel, activeSessionId, createSession]); - - const currentModel = modelData?.find(m => m.id === selectedModel); - const currentDiscoveryModel = discoveryData?.data?.find(m => m.id === selectedModel); - - // Use parameters from discovery model - const modelParameters = currentDiscoveryModel?.parameters ?? '{}'; - const parameterState = useParameterState({ - parameters: modelParameters, - persistKey: `chat-params-${selectedModel ?? 'default'}`, - }); - - // Use streaming logic hook - const { sendMessage, abortControllerRef } = useChatStreamingLogic({ - selectedModel, - messages, - setMessages, - isLoading, - setIsLoading, - setStreamingContent, - setTokensPerSecond, - setError, - getActiveSession, - performanceSettings, - dynamicParameters: parameterState.getSubmitValues(), - }); - - // Cleanup on unmount - abort any pending requests - useEffect(() => { - return () => { - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - abortControllerRef.current = null; - } - }; - }, [abortControllerRef]); - - if (modelsLoading) { - return ( -
- -
- ); - } - - if (error) { - return ( - - { - setError(null); - setIsLoading(false); - }} - actions={[ - { - label: 'Configure Providers', - onClick: () => window.location.href = '/llm-providers', - color: 'blue', - variant: 'light', - } - ]} - /> - - ); - } - - if (!modelData || modelData.length === 0) { - return ( - - } color="yellow" title="No models available"> - No models are currently configured. Please add model mappings first.
- Add model mappings -
-
- ); - } - - return ( - - - - - - - - {currentModel?.supportsVision && ( - - Vision Enabled - - )} - {currentModel && messages.length > 0 && ( - - )} - - setShowSettings(!showSettings)} - aria-label="Toggle advanced settings" - > - {showSettings ? : } - - - - - - - - {/* Token Counter */} - {currentModel && ( - - )} - - - - {/* Dynamic Parameters UI */} - {currentDiscoveryModel?.parameters && currentDiscoveryModel.parameters !== '{}' && ( - - )} - - - - - - - - - void sendMessage(message, images)} - isStreaming={isLoading} - onStopStreaming={() => {}} - disabled={!selectedModel} - model={currentModel ? { - id: currentModel.id, - providerId: currentModel.providerId || '', - displayName: currentModel.displayName, - supportsVision: currentModel.supportsVision - } : undefined} - /> - - - - ); -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/chat/components/ChatMessages.tsx b/ConduitLLM.WebUI/src/app/chat/components/ChatMessages.tsx deleted file mode 100755 index 9ac08144b..000000000 --- a/ConduitLLM.WebUI/src/app/chat/components/ChatMessages.tsx +++ /dev/null @@ -1,484 +0,0 @@ -import { ScrollArea, Stack, Text, Group, Badge, Paper, Code, Collapse, ActionIcon, Alert, HoverCard, CopyButton, Tooltip } from '@mantine/core'; -import { IconUser, IconRobot, IconClock, IconBolt, IconAlertCircle, IconNetwork, IconLock, IconSearch, IconAlertTriangle, IconChevronDown, IconChevronUp, IconInfoCircle, IconCopy, IconCheck } from '@tabler/icons-react'; -import { ChatMessage, ChatErrorType } from '../types'; -import React, { useEffect, useRef, useState } from 'react'; -import ReactMarkdown from 'react-markdown'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'; -import { ImagePreview } from './ImagePreview'; -import { processStructuredContent, getBlockQuoteMetadata, cleanBlockQuoteContent } from '@knn_labs/conduit-core-client'; - -interface ChatMessagesProps { - messages: ChatMessage[]; - streamingContent?: string; - tokensPerSecond?: number | null; -} - -// Helper function to get error type styling -function getErrorTypeConfig(type: ChatErrorType) { - switch (type) { - case 'rate_limit': - return { icon: IconClock, color: 'orange', label: 'Rate Limit' }; - case 'model_not_found': - return { icon: IconSearch, color: 'blue', label: 'Model Not Found' }; - case 'auth_error': - return { icon: IconLock, color: 'red', label: 'Authentication Error' }; - case 'network_error': - return { icon: IconNetwork, color: 'gray', label: 'Network Error' }; - case 'server_error': - default: - return { icon: IconAlertTriangle, color: 'red', label: 'Server Error' }; - } -} - -export function ChatMessages({ messages, streamingContent, tokensPerSecond }: ChatMessagesProps) { - const scrollAreaRef = useRef(null); - const lastMessageRef = useRef(null); - const [expandedErrors, setExpandedErrors] = useState>(new Set()); - - useEffect(() => { - if (lastMessageRef.current) { - lastMessageRef.current.scrollIntoView({ behavior: 'smooth' }); - } - }, [messages, streamingContent]); - - const toggleErrorDetails = (messageId: string) => { - setExpandedErrors(prev => { - const next = new Set(prev); - if (next.has(messageId)) { - next.delete(messageId); - } else { - next.add(messageId); - } - return next; - }); - }; - - // Component for collapsible thinking blocks - const CollapsibleThinking = ({ content, icon, title }: { content: string; icon: string; title: string }) => { - const [isOpen, setIsOpen] = useState(false); - - return ( - setIsOpen(!isOpen)} - > - - - {isOpen ? : } - - - {icon} {title} - - - {isOpen ? 'Click to collapse' : 'Click to expand'} - - - -
- {content} -
-
-
- ); - }; - - const renderMessage = (message: ChatMessage, isStreaming = false) => { - const isUser = message.role === 'user'; - const content = isStreaming ? streamingContent : message.content; - const hasError = message.error && !isUser; - const errorConfig = hasError && message.error ? getErrorTypeConfig(message.error.type) : null; - const isExpanded = expandedErrors.has(message.id); - - // For error messages, render special error UI - if (hasError && errorConfig && message.error) { - const Icon = errorConfig.icon; - return ( - - - {/* Error header with icon and type */} - - - - - {errorConfig.label} - - - {message.error.retryAfter && ( - - Retry after {message.error.retryAfter}s - - )} - - - {/* User-friendly error message */} - - {content?.replace('Error: ', '')} - - - {/* Suggestions if available */} - {message.error.suggestions && message.error.suggestions.length > 0 && ( - } color={errorConfig.color} variant="light"> - - Suggestions: - {message.error.suggestions.map((suggestion) => ( - • {suggestion} - ))} - - - )} - - {/* Technical details (expandable) */} - {(message.error.technical ?? message.error.code ?? message.error.statusCode) && ( - <> - - toggleErrorDetails(message.id)} - > - {isExpanded ? : } - - Technical Details - - - - - {message.error.statusCode && ( - - HTTP Status: {message.error.statusCode} - - )} - {message.error.code && ( - - Error Code: {message.error.code} - - )} - {message.error.technical && ( - - {message.error.technical} - - )} - - - - - )} - - - ); - } - - // Regular message rendering - return ( - - - - - {isUser ? ( - - ) : ( - - )} - - {isUser ? 'You' : message.model ?? 'Assistant'} - - - - {!isUser && (message.metadata ?? (isStreaming && tokensPerSecond)) && ( - - {message.metadata?.tokensUsed !== null && message.metadata?.tokensUsed !== undefined && message.metadata.tokensUsed > 0 && ( - - {message.metadata.tokensUsed} tokens - - )} - {(() => { - const tps = message.metadata?.tokensPerSecond ?? (isStreaming ? tokensPerSecond : null); - return tps !== null && tps !== undefined && tps > 0 ? ( - - - - {tps.toFixed(1)} t/s - - - ) : null; - })()} - {message.metadata?.latency !== null && message.metadata?.latency !== undefined && message.metadata.latency > 0 && ( - - - - {(message.metadata.latency / 1000).toFixed(1)}s - - - )} - {/* Metadata hover card */} - {(message.metadata?.provider ?? message.metadata?.model ?? message.metadata?.promptTokens ?? message.metadata?.completionTokens) && ( - - - - - - - - - Response Details - {message.metadata.streaming !== undefined && ( - - Response Type: - {message.metadata.streaming ? 'SSE (Streaming)' : 'JSON (Complete)'} - - )} - {message.metadata.provider && ( - - Provider: - {message.metadata.provider} - - )} - {message.metadata.model && ( - - Model: - {message.metadata.model} - - )} - {message.metadata.promptTokens !== undefined && ( - - Prompt Tokens: - {message.metadata.promptTokens} - - )} - {message.metadata.completionTokens !== undefined && ( - - Completion Tokens: - {message.metadata.completionTokens} - - )} - - - - )} - - )} - - - {message.images && message.images.length > 0 && ( - - )} - - {message.functionCall && ( - - Function Call: - {message.functionCall.name} - - {message.functionCall.arguments} - - - )} - - {message.toolCalls && message.toolCalls.length > 0 && ( - - Tool Calls: - {message.toolCalls.map((tool) => ( - - {tool.function.name} - - {tool.function.arguments} - - - ))} - - )} - -
- { - if (typeof node === 'string') return node; - if (typeof node === 'number') return node.toString(); - if (Array.isArray(node)) return node.map(getChildrenText).join(''); - return ''; - }; - - const childText = getChildrenText(children); - - return !inline && match ? ( - )} - > - {childText.replace(/\n$/, '')} - - ) : ( - - {childText} - - ); - }, - blockquote({ children, ...props }) { - const getChildrenText = (node: React.ReactNode): string => { - if (typeof node === 'string') return node; - if (typeof node === 'number') return node.toString(); - if (Array.isArray(node)) return node.map(getChildrenText).join(''); - if (!node || typeof node !== 'object') return ''; - - // Type guard for React element - if (React.isValidElement(node)) { - const element = node as React.ReactElement<{children?: React.ReactNode}>; - if (element.props && element.props.children !== undefined) { - return getChildrenText(element.props.children); - } - } - return ''; - }; - - const text = getChildrenText(children); - const metadata = getBlockQuoteMetadata(text); - - // Handle thinking blocks with collapsible UI - if (metadata.type === 'thinking') { - const cleanedContent = cleanBlockQuoteContent(text); - - return ( - - ); - } - - // Handle warning blocks - if (metadata.type === 'warning') { - const cleanedContent = cleanBlockQuoteContent(text); - - return ( - } - color="orange" - variant="light" - radius="md" - > - {cleanedContent} - - ); - } - - // Handle summary blocks - if (metadata.type === 'summary') { - const cleanedContent = cleanBlockQuoteContent(text); - - return ( - - - {metadata.icon} {metadata.title} - - {cleanedContent} - - ); - } - - // Default blockquote - return
{children}
; - }, - }} - > - {processStructuredContent(content ?? '')} -
-
- - {/* Copy button */} - {content && ( - - - {({ copied, copy }) => ( - - - {copied ? : } - - - )} - - - )} -
-
- ); - }; - - return ( - - - {messages.map((message) => renderMessage(message))} - {streamingContent && renderMessage( - { - id: 'streaming', - role: 'assistant', - content: '', - timestamp: new Date(), - }, - true - )} -
- - - ); -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/chat/components/ChatSettings.tsx b/ConduitLLM.WebUI/src/app/chat/components/ChatSettings.tsx deleted file mode 100755 index 44091eacd..000000000 --- a/ConduitLLM.WebUI/src/app/chat/components/ChatSettings.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { - ActionIcon, - Stack, - Slider, - Text, - NumberInput, - Select, - Textarea, - Switch, - Group, - Divider, - Badge -} from '@mantine/core'; -import { IconRefresh } from '@tabler/icons-react'; -import { useChatStore } from '../hooks/useChatStore'; -import { CHAT_PRESETS, findMatchingPreset, getPresetIcon } from '../utils/presets'; -import { ChatParameters } from '../types'; - -export function ChatSettings() { - const { getActiveSession, updateSessionParameters } = useChatStore(); - const activeSession = getActiveSession(); - - if (!activeSession) return null; - - const parameters = activeSession.parameters; - - const handleParameterChange = (updates: Partial) => { - updateSessionParameters(activeSession.id, updates); - }; - - const handlePresetSelect = (presetId: string | null) => { - if (!presetId) return; - - const preset = CHAT_PRESETS.find(p => p.id === presetId); - if (preset) { - // The preset parameters from SDK match our ChatParameters interface - handleParameterChange({ - temperature: preset.parameters.temperature, - topP: preset.parameters.topP, - frequencyPenalty: preset.parameters.frequencyPenalty, - presencePenalty: preset.parameters.presencePenalty, - }); - } - }; - - const resetToDefaults = () => { - handleParameterChange({ - temperature: 0.7, - maxTokens: 2048, - topP: 1, - frequencyPenalty: 0, - presencePenalty: 0, - responseFormat: 'text', - seed: undefined, - stop: undefined, - stream: true, - }); - }; - - return ( - - - Chat Settings - - - - - - handleParameterChange({ responseFormat: value as 'text' | 'json_object' })} - data={[ - { value: 'text', label: 'Text' }, - { value: 'json_object', label: 'JSON' }, - ]} - /> - - handleParameterChange({ stream: event.currentTarget.checked })} - description="Stream responses as they are generated" - /> - - handleParameterChange({ seed: value ? Number(value) : undefined })} - min={0} - /> - -