diff --git a/.dockerignore b/.dockerignore index 3ac33017e..bf4c6f154 100644 --- a/.dockerignore +++ b/.dockerignore @@ -27,6 +27,8 @@ yarn-error.log* # Data directories (PostgreSQL, Redis persistent data) data/redis data/postgres +data/redis-dev +data/postgres-dev # Documentation submodule (not needed in container) docs-site diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 09bc0cd80..6e06f3664 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -62,10 +62,12 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 - - name: Install dependencies and format code + - name: Install dependencies, type check, and format code run: | bun install + bun run typecheck bun run format + bun run lint - name: Commit formatted code run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ce833172..a495efb80 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -198,14 +198,15 @@ jobs: if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' uses: oven-sh/setup-bun@v2 - - name: Install dependencies and format code + - name: Install dependencies, type check, and format code if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' run: | bun install + bun run typecheck bun run format - name: Commit VERSION and formatted code - if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' + if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && success() run: | # 配置git git config user.name "github-actions[bot]" @@ -226,7 +227,7 @@ jobs: fi - name: Create and push tag - if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' + if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && success() run: | NEW_TAG="${{ steps.next_version.outputs.new_tag }}" git tag -a "$NEW_TAG" -m "Release $NEW_TAG" @@ -234,14 +235,14 @@ jobs: - name: Prepare image names id: image_names - if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' + if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && success() run: | GHCR_IMAGE=$(echo "ghcr.io/${{ github.repository_owner }}/claude-code-hub" | tr '[:upper:]' '[:lower:]') echo "ghcr_image=${GHCR_IMAGE}" >> "$GITHUB_OUTPUT" - name: Create GitHub Release - if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' + if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && success() uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.next_version.outputs.new_tag }} @@ -263,7 +264,7 @@ jobs: # 自清理旧的tags和releases(保持最近50个) - 仅清理正式版本 - name: Cleanup old tags and releases - if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && steps.next_version.outputs.is_prerelease != 'true' + if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && steps.next_version.outputs.is_prerelease != 'true' && success() continue-on-error: true env: TAGS_TO_KEEP: 50 @@ -364,15 +365,15 @@ jobs: # Docker构建步骤 - name: Set up QEMU - if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' + if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && success() uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' + if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && success() uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry - if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' + if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && success() uses: docker/login-action@v3 with: registry: ghcr.io @@ -380,7 +381,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image - if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' + if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && success() uses: docker/build-push-action@v6 with: context: . @@ -402,7 +403,7 @@ jobs: # 同步 main 分支到 dev 分支 (rebase dev onto main) - 仅正式版本触发 - name: Sync main to dev branch - if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && steps.next_version.outputs.is_prerelease != 'true' + if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && steps.next_version.outputs.is_prerelease != 'true' && success() id: sync_dev continue-on-error: true env: @@ -480,7 +481,7 @@ jobs: # 如果同步失败(冲突),触发 autofix workflow - name: Trigger autofix for sync conflicts - if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && steps.sync_dev.outputs.sync_status == 'conflict' + if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && steps.sync_dev.outputs.sync_status == 'conflict' && success() env: GH_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} run: | diff --git a/CLAUDE.md b/CLAUDE.md index cf1b28607..15049fc97 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,15 +26,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ```bash # Development bun install # Install dependencies -bun run dev # Start dev server (port 13500) +bun run dev # Run tsgo preflight, then start dev server (port 13500) # Build & Production -bun run build # Build for production (copies VERSION to standalone) +bun run build # Run tsgo preflight, then build for production bun run start # Start production server # Quality Checks bun run typecheck # Type check with tsgo (faster) -bun run typecheck:tsc # Type check with tsc bun run lint # Lint with Biome bun run lint:fix # Auto-fix lint issues bun run format # Format code diff --git a/drizzle/0085_busy_ken_ellis.sql b/drizzle/0085_busy_ken_ellis.sql new file mode 100644 index 000000000..aef2673f2 --- /dev/null +++ b/drizzle/0085_busy_ken_ellis.sql @@ -0,0 +1 @@ +ALTER TABLE "system_settings" ADD COLUMN "codex_priority_billing_source" varchar(20) DEFAULT 'requested' NOT NULL; diff --git a/drizzle/meta/0085_snapshot.json b/drizzle/meta/0085_snapshot.json new file mode 100644 index 000000000..e897af375 --- /dev/null +++ b/drizzle/meta/0085_snapshot.json @@ -0,0 +1,4009 @@ +{ + "id": "70edcf0a-ce4e-4951-9ebf-d92e95632f7c", + "prevId": "1d1ba01c-7adc-467e-bc9c-90abab9e5b92", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.error_rules": { + "name": "error_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'regex'" + }, + "category": { + "name": "category", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "override_response": { + "name": "override_response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "override_status_code": { + "name": "override_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_error_rules_enabled": { + "name": "idx_error_rules_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_pattern": { + "name": "unique_pattern", + "columns": [ + { + "expression": "pattern", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_category": { + "name": "idx_category", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_match_type": { + "name": "idx_match_type", + "columns": [ + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.keys": { + "name": "keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "can_login_web_ui": { + "name": "can_login_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "cost_reset_at": { + "name": "cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_keys_user_id": { + "name": "idx_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_key": { + "name": "idx_keys_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_created_at": { + "name": "idx_keys_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_deleted_at": { + "name": "idx_keys_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_request": { + "name": "message_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(21, 15)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "request_sequence": { + "name": "request_sequence", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "provider_chain": { + "name": "provider_chain", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "api_type": { + "name": "api_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "original_model": { + "name": "original_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "swap_cache_ttl_applied": { + "name": "swap_cache_ttl_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "special_settings": { + "name": "special_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_stack": { + "name": "error_stack", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_cause": { + "name": "error_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_by": { + "name": "blocked_by", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "messages_count": { + "name": "messages_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_message_request_user_date_cost": { + "name": "idx_message_request_user_date_cost", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_created_at_cost_stats": { + "name": "idx_message_request_user_created_at_cost_stats", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_query": { + "name": "idx_message_request_user_query", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_active": { + "name": "idx_message_request_provider_created_at_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_id": { + "name": "idx_message_request_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_id_prefix": { + "name": "idx_message_request_session_id_prefix", + "columns": [ + { + "expression": "\"session_id\" varchar_pattern_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_seq": { + "name": "idx_message_request_session_seq", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_endpoint": { + "name": "idx_message_request_endpoint", + "columns": [ + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_blocked_by": { + "name": "idx_message_request_blocked_by", + "columns": [ + { + "expression": "blocked_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_id": { + "name": "idx_message_request_provider_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_id": { + "name": "idx_message_request_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key": { + "name": "idx_message_request_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_created_at_id": { + "name": "idx_message_request_key_created_at_id", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_model_active": { + "name": "idx_message_request_key_model_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_endpoint_active": { + "name": "idx_message_request_key_endpoint_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"endpoint\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at_id_active": { + "name": "idx_message_request_created_at_id_active", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_model_active": { + "name": "idx_message_request_model_active", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_status_code_active": { + "name": "idx_message_request_status_code_active", + "columns": [ + { + "expression": "status_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at": { + "name": "idx_message_request_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_deleted_at": { + "name": "idx_message_request_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_last_active": { + "name": "idx_message_request_key_last_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_cost_active": { + "name": "idx_message_request_key_cost_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_user_info": { + "name": "idx_message_request_session_user_info", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_prices": { + "name": "model_prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "model_name": { + "name": "model_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "price_data": { + "name": "price_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'litellm'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_model_prices_latest": { + "name": "idx_model_prices_latest", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_model_name": { + "name": "idx_model_prices_model_name", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_created_at": { + "name": "idx_model_prices_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_source": { + "name": "idx_model_prices_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "use_legacy_mode": { + "name": "use_legacy_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_enabled": { + "name": "circuit_breaker_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_webhook": { + "name": "circuit_breaker_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_enabled": { + "name": "daily_leaderboard_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "daily_leaderboard_webhook": { + "name": "daily_leaderboard_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_time": { + "name": "daily_leaderboard_time", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'09:00'" + }, + "daily_leaderboard_top_n": { + "name": "daily_leaderboard_top_n", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "cost_alert_enabled": { + "name": "cost_alert_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cost_alert_webhook": { + "name": "cost_alert_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cost_alert_threshold": { + "name": "cost_alert_threshold", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0.80'" + }, + "cost_alert_check_interval": { + "name": "cost_alert_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 60 + }, + "cache_hit_rate_alert_enabled": { + "name": "cache_hit_rate_alert_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cache_hit_rate_alert_webhook": { + "name": "cache_hit_rate_alert_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cache_hit_rate_alert_window_mode": { + "name": "cache_hit_rate_alert_window_mode", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'auto'" + }, + "cache_hit_rate_alert_check_interval": { + "name": "cache_hit_rate_alert_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "cache_hit_rate_alert_historical_lookback_days": { + "name": "cache_hit_rate_alert_historical_lookback_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "cache_hit_rate_alert_min_eligible_requests": { + "name": "cache_hit_rate_alert_min_eligible_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 20 + }, + "cache_hit_rate_alert_min_eligible_tokens": { + "name": "cache_hit_rate_alert_min_eligible_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "cache_hit_rate_alert_abs_min": { + "name": "cache_hit_rate_alert_abs_min", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "cache_hit_rate_alert_drop_rel": { + "name": "cache_hit_rate_alert_drop_rel", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.3'" + }, + "cache_hit_rate_alert_drop_abs": { + "name": "cache_hit_rate_alert_drop_abs", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.1'" + }, + "cache_hit_rate_alert_cooldown_minutes": { + "name": "cache_hit_rate_alert_cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "cache_hit_rate_alert_top_n": { + "name": "cache_hit_rate_alert_top_n", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_target_bindings": { + "name": "notification_target_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "schedule_cron": { + "name": "schedule_cron", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "schedule_timezone": { + "name": "schedule_timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "template_override": { + "name": "template_override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "unique_notification_target_binding": { + "name": "unique_notification_target_binding", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_type": { + "name": "idx_notification_bindings_type", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_target": { + "name": "idx_notification_bindings_target", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_target_bindings_target_id_webhook_targets_id_fk": { + "name": "notification_target_bindings_target_id_webhook_targets_id_fk", + "tableFrom": "notification_target_bindings", + "tableTo": "webhook_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoint_probe_logs": { + "name": "provider_endpoint_probe_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_type": { + "name": "error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_provider_endpoint_probe_logs_endpoint_created_at": { + "name": "idx_provider_endpoint_probe_logs_endpoint_created_at", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoint_probe_logs_created_at": { + "name": "idx_provider_endpoint_probe_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk": { + "name": "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk", + "tableFrom": "provider_endpoint_probe_logs", + "tableTo": "provider_endpoints", + "columnsFrom": [ + "endpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoints": { + "name": "provider_endpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_probed_at": { + "name": "last_probed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_probe_ok": { + "name": "last_probe_ok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "last_probe_status_code": { + "name": "last_probe_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_latency_ms": { + "name": "last_probe_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_type": { + "name": "last_probe_error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_message": { + "name": "last_probe_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uniq_provider_endpoints_vendor_type_url": { + "name": "uniq_provider_endpoints_vendor_type_url", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_vendor_type": { + "name": "idx_provider_endpoints_vendor_type", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_enabled": { + "name": "idx_provider_endpoints_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_pick_enabled": { + "name": "idx_provider_endpoints_pick_enabled", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_created_at": { + "name": "idx_provider_endpoints_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_deleted_at": { + "name": "idx_provider_endpoints_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoints_vendor_id_provider_vendors_id_fk": { + "name": "provider_endpoints_vendor_id_provider_vendors_id_fk", + "tableFrom": "provider_endpoints", + "tableTo": "provider_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_vendors": { + "name": "provider_vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "website_domain": { + "name": "website_domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "uniq_provider_vendors_website_domain": { + "name": "uniq_provider_vendors_website_domain", + "columns": [ + { + "expression": "website_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_vendors_created_at": { + "name": "idx_provider_vendors_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.providers": { + "name": "providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider_vendor_id": { + "name": "provider_vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "group_priorities": { + "name": "group_priorities", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false, + "default": "'1.0'" + }, + "group_tag": { + "name": "group_tag", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "preserve_client_ip": { + "name": "preserve_client_ip", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "model_redirects": { + "name": "model_redirects", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "blocked_clients": { + "name": "blocked_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "active_time_start": { + "name": "active_time_start", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "active_time_end": { + "name": "active_time_end", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "codex_instructions_strategy": { + "name": "codex_instructions_strategy", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'auto'" + }, + "mcp_passthrough_type": { + "name": "mcp_passthrough_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "mcp_passthrough_url": { + "name": "mcp_passthrough_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "total_cost_reset_at": { + "name": "total_cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "max_retry_attempts": { + "name": "max_retry_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "circuit_breaker_failure_threshold": { + "name": "circuit_breaker_failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "circuit_breaker_open_duration": { + "name": "circuit_breaker_open_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1800000 + }, + "circuit_breaker_half_open_success_threshold": { + "name": "circuit_breaker_half_open_success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2 + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "first_byte_timeout_streaming_ms": { + "name": "first_byte_timeout_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "streaming_idle_timeout_ms": { + "name": "streaming_idle_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_timeout_non_streaming_ms": { + "name": "request_timeout_non_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "swap_cache_ttl_billing": { + "name": "swap_cache_ttl_billing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "context_1m_preference": { + "name": "context_1m_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_effort_preference": { + "name": "codex_reasoning_effort_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_summary_preference": { + "name": "codex_reasoning_summary_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_text_verbosity_preference": { + "name": "codex_text_verbosity_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_parallel_tool_calls_preference": { + "name": "codex_parallel_tool_calls_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_service_tier_preference": { + "name": "codex_service_tier_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_max_tokens_preference": { + "name": "anthropic_max_tokens_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_thinking_budget_preference": { + "name": "anthropic_thinking_budget_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_adaptive_thinking": { + "name": "anthropic_adaptive_thinking", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "gemini_google_search_preference": { + "name": "gemini_google_search_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "tpm": { + "name": "tpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "rpm": { + "name": "rpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "rpd": { + "name": "rpd", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "cc": { + "name": "cc", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_providers_enabled_priority": { + "name": "idx_providers_enabled_priority", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "weight", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_group": { + "name": "idx_providers_group", + "columns": [ + { + "expression": "group_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_vendor_type_url_active": { + "name": "idx_providers_vendor_type_url_active", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_created_at": { + "name": "idx_providers_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_deleted_at": { + "name": "idx_providers_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_vendor_type": { + "name": "idx_providers_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_enabled_vendor_type": { + "name": "idx_providers_enabled_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL AND \"providers\".\"is_enabled\" = true AND \"providers\".\"provider_vendor_id\" IS NOT NULL AND \"providers\".\"provider_vendor_id\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "providers_provider_vendor_id_provider_vendors_id_fk": { + "name": "providers_provider_vendor_id_provider_vendors_id_fk", + "tableFrom": "providers", + "tableTo": "provider_vendors", + "columnsFrom": [ + "provider_vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.request_filters": { + "name": "request_filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "binding_type": { + "name": "binding_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'global'" + }, + "provider_ids": { + "name": "provider_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "group_tags": { + "name": "group_tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rule_mode": { + "name": "rule_mode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'simple'" + }, + "execution_phase": { + "name": "execution_phase", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'guard'" + }, + "operations": { + "name": "operations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_request_filters_enabled": { + "name": "idx_request_filters_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_scope": { + "name": "idx_request_filters_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_action": { + "name": "idx_request_filters_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_binding": { + "name": "idx_request_filters_binding", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_phase": { + "name": "idx_request_filters_phase", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_phase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensitive_words": { + "name": "sensitive_words", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "word": { + "name": "word", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'contains'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_sensitive_words_enabled": { + "name": "idx_sensitive_words_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sensitive_words_created_at": { + "name": "idx_sensitive_words_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_settings": { + "name": "system_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_title": { + "name": "site_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "default": "'Claude Code Hub'" + }, + "allow_global_usage_view": { + "name": "allow_global_usage_view", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "currency_display": { + "name": "currency_display", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "billing_model_source": { + "name": "billing_model_source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'original'" + }, + "codex_priority_billing_source": { + "name": "codex_priority_billing_source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "timezone": { + "name": "timezone", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "enable_auto_cleanup": { + "name": "enable_auto_cleanup", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "cleanup_retention_days": { + "name": "cleanup_retention_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "cleanup_schedule": { + "name": "cleanup_schedule", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'0 2 * * *'" + }, + "cleanup_batch_size": { + "name": "cleanup_batch_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10000 + }, + "enable_client_version_check": { + "name": "enable_client_version_check", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verbose_provider_error": { + "name": "verbose_provider_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_http2": { + "name": "enable_http2", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "intercept_anthropic_warmup_requests": { + "name": "intercept_anthropic_warmup_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_thinking_signature_rectifier": { + "name": "enable_thinking_signature_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_thinking_budget_rectifier": { + "name": "enable_thinking_budget_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_billing_header_rectifier": { + "name": "enable_billing_header_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_input_rectifier": { + "name": "enable_response_input_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_codex_session_id_completion": { + "name": "enable_codex_session_id_completion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_claude_metadata_user_id_injection": { + "name": "enable_claude_metadata_user_id_injection", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_fixer": { + "name": "enable_response_fixer", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "response_fixer_config": { + "name": "response_fixer_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"fixTruncatedJson\":true,\"fixSseFormat\":true,\"fixEncoding\":true,\"maxJsonDepth\":200,\"maxFixSize\":1048576}'::jsonb" + }, + "quota_db_refresh_interval_seconds": { + "name": "quota_db_refresh_interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "quota_lease_percent_5h": { + "name": "quota_lease_percent_5h", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_daily": { + "name": "quota_lease_percent_daily", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_weekly": { + "name": "quota_lease_percent_weekly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_monthly": { + "name": "quota_lease_percent_monthly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_cap_usd": { + "name": "quota_lease_cap_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_ledger": { + "name": "usage_ledger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "final_provider_id": { + "name": "final_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "original_model": { + "name": "original_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "api_type": { + "name": "api_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_success": { + "name": "is_success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "blocked_by": { + "name": "blocked_by", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(21, 15)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "swap_cache_ttl_applied": { + "name": "swap_cache_ttl_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_usage_ledger_request_id": { + "name": "idx_usage_ledger_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_created_at": { + "name": "idx_usage_ledger_user_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_created_at": { + "name": "idx_usage_ledger_key_created_at", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_provider_created_at": { + "name": "idx_usage_ledger_provider_created_at", + "columns": [ + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_created_at_minute": { + "name": "idx_usage_ledger_created_at_minute", + "columns": [ + { + "expression": "date_trunc('minute', \"created_at\" AT TIME ZONE 'UTC')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_created_at_desc_id": { + "name": "idx_usage_ledger_created_at_desc_id", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_session_id": { + "name": "idx_usage_ledger_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"session_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_model": { + "name": "idx_usage_ledger_model", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_cost": { + "name": "idx_usage_ledger_key_cost", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_cost_cover": { + "name": "idx_usage_ledger_user_cost_cover", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_provider_cost_cover": { + "name": "idx_usage_ledger_provider_cost_cover", + "columns": [ + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_created_at_desc_cover": { + "name": "idx_usage_ledger_key_created_at_desc_cover", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "rpm_limit": { + "name": "rpm_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "daily_limit_usd": { + "name": "daily_limit_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "cost_reset_at": { + "name": "cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "blocked_clients": { + "name": "blocked_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_users_active_role_sort": { + "name": "idx_users_active_role_sort", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_enabled_expires_at": { + "name": "idx_users_enabled_expires_at", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_tags_gin": { + "name": "idx_users_tags_gin", + "columns": [ + { + "expression": "tags", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_users_created_at": { + "name": "idx_users_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_deleted_at": { + "name": "idx_users_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_targets": { + "name": "webhook_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "webhook_provider_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "webhook_url": { + "name": "webhook_url", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "telegram_bot_token": { + "name": "telegram_bot_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "dingtalk_secret": { + "name": "dingtalk_secret", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "custom_template": { + "name": "custom_template", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_test_at": { + "name": "last_test_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_result": { + "name": "last_test_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.daily_reset_mode": { + "name": "daily_reset_mode", + "schema": "public", + "values": [ + "fixed", + "rolling" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "circuit_breaker", + "daily_leaderboard", + "cost_alert", + "cache_hit_rate_alert" + ] + }, + "public.webhook_provider_type": { + "name": "webhook_provider_type", + "schema": "public", + "values": [ + "wechat", + "feishu", + "dingtalk", + "telegram", + "custom" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 53179554f..e4a2ea63e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -596,6 +596,13 @@ "when": 1773809022650, "tag": "0084_needy_jackpot", "breakpoints": true + }, + { + "idx": 85, + "version": "7", + "when": 1774098220127, + "tag": "0085_busy_ken_ellis", + "breakpoints": true } ] } \ No newline at end of file diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index acb2bbc91..4d6de4678 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -95,6 +95,8 @@ "exporting": "Exporting...", "exportSuccess": "Export completed successfully", "exportError": "Export failed", + "exportPreparing": "Preparing export...", + "exportProgress": "Exported {current} / {total}", "quickFilters": { "today": "Today", "thisWeek": "This Week", diff --git a/messages/en/settings/config.json b/messages/en/settings/config.json index 97ff8ca99..6a700ada0 100644 --- a/messages/en/settings/config.json +++ b/messages/en/settings/config.json @@ -13,6 +13,13 @@ "redirected": "After Redirection (Actual Model)" }, "billingModelSourcePlaceholder": "Select billing model source", + "codexPriorityBillingSource": "Codex Priority Billing Source", + "codexPriorityBillingSourceDesc": "Controls which service_tier is used for Codex Priority (Fast Mode) surcharge billing. The default is Requested Service Tier; if Actual Service Tier is selected, the response value is used first and falls back to the request value when the response omits it.", + "codexPriorityBillingSourceOptions": { + "requested": "Requested Service Tier (Default)", + "actual": "Actual Service Tier (Fallback to Requested)" + }, + "codexPriorityBillingSourcePlaceholder": "Select Codex Priority billing source", "cleanupBatchSize": "Batch Size", "cleanupBatchSizeDesc": "Number of records to delete per batch (range: 1000-100000, recommended 10000)", "cleanupBatchSizePlaceholder": "10000", diff --git a/messages/en/settings/providers/form/sections.json b/messages/en/settings/providers/form/sections.json index b26aa58f1..f05a1e89e 100644 --- a/messages/en/settings/providers/form/sections.json +++ b/messages/en/settings/providers/form/sections.json @@ -101,10 +101,10 @@ }, "title": "Proxy", "url": { - "formats": "Supported formats:", + "formats": "Supports http://, https://, socks5://, socks4:// protocols. For authenticated proxies use http://user:password@host:port (URL-encode special characters in password, e.g. # as %23)", "label": "Proxy URL", "optional": "(optional)", - "placeholder": "e.g. http://proxy.example.com:8080 or socks5://127.0.0.1:1080" + "placeholder": "e.g. http://proxy.example.com:8080 or http://user:pass@proxy:8080" } }, "rateLimit": { diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index 1d9428d7c..bd59bc42f 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -95,6 +95,8 @@ "exporting": "エクスポート中...", "exportSuccess": "エクスポートが完了しました", "exportError": "エクスポートに失敗しました", + "exportPreparing": "エクスポートを準備中...", + "exportProgress": "{current} / {total} 件をエクスポート済み", "quickFilters": { "today": "今日", "thisWeek": "今週", diff --git a/messages/ja/settings/config.json b/messages/ja/settings/config.json index 24b9f7b72..c4038c90f 100644 --- a/messages/ja/settings/config.json +++ b/messages/ja/settings/config.json @@ -13,6 +13,13 @@ "redirected": "リダイレクト後(実際のモデル)" }, "billingModelSourcePlaceholder": "課金モデルソースを選択", + "codexPriorityBillingSource": "Codex Priority 課金参照元", + "codexPriorityBillingSourceDesc": "Codex Priority(Fast Mode)の追加課金に使う service_tier を制御します。デフォルトは Requested Service Tier です。Actual Service Tier を選ぶとレスポンス値を優先し、レスポンスに無い場合はリクエスト値へフォールバックします。", + "codexPriorityBillingSourceOptions": { + "requested": "Requested Service Tier(デフォルト)", + "actual": "Actual Service Tier(無い場合は Requested へフォールバック)" + }, + "codexPriorityBillingSourcePlaceholder": "Codex Priority の課金参照元を選択", "cleanupBatchSize": "バッチサイズ", "cleanupBatchSizeDesc": "バッチごとに削除するレコード数(範囲:1000-100000、推奨10000)", "cleanupBatchSizePlaceholder": "10000", diff --git a/messages/ja/settings/providers/form/sections.json b/messages/ja/settings/providers/form/sections.json index 6f93c7121..57b6ec123 100644 --- a/messages/ja/settings/providers/form/sections.json +++ b/messages/ja/settings/providers/form/sections.json @@ -101,10 +101,10 @@ }, "title": "プロキシ設定", "url": { - "formats": "対応フォーマット:", + "formats": "http://、https://、socks5://、socks4:// プロトコルに対応。認証が必要な場合は http://user:password@host:port 形式を使用してください(パスワード内の特殊文字は URL エンコードが必要です。例: # → %23)", "label": "プロキシ URL", "optional": "(任意)", - "placeholder": "例: http://proxy.example.com:8080 または socks5://127.0.0.1:1080" + "placeholder": "例: http://proxy.example.com:8080、http://user:pass@proxy:8080、socks5://proxy.example.com:1080" } }, "rateLimit": { diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index f7be150d5..c307023a3 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -95,6 +95,8 @@ "exporting": "Экспорт...", "exportSuccess": "Экспорт завершен", "exportError": "Ошибка экспорта", + "exportPreparing": "Подготовка экспорта...", + "exportProgress": "Экспортировано {current} / {total}", "quickFilters": { "today": "Сегодня", "thisWeek": "Эта неделя", diff --git a/messages/ru/settings/config.json b/messages/ru/settings/config.json index ae0f32e13..adf0a6a7f 100644 --- a/messages/ru/settings/config.json +++ b/messages/ru/settings/config.json @@ -13,6 +13,13 @@ "redirected": "После перенаправления (фактическая модель)" }, "billingModelSourcePlaceholder": "Выберите источник модели для тарификации", + "codexPriorityBillingSource": "Источник тарификации Codex Priority", + "codexPriorityBillingSourceDesc": "Определяет, какой service_tier использовать для отдельной тарификации Codex Priority (Fast Mode). По умолчанию используется Requested Service Tier; если выбран Actual Service Tier, сначала берется значение из ответа, а при его отсутствии используется значение из запроса.", + "codexPriorityBillingSourceOptions": { + "requested": "Requested Service Tier (по умолчанию)", + "actual": "Actual Service Tier (с откатом к Requested)" + }, + "codexPriorityBillingSourcePlaceholder": "Выберите источник тарификации Codex Priority", "cleanupBatchSize": "Размер пакета", "cleanupBatchSizeDesc": "Количество записей для удаления за раз (диапазон: 1000-100000, рекомендуется 10000)", "cleanupBatchSizePlaceholder": "10000", diff --git a/messages/ru/settings/providers/form/sections.json b/messages/ru/settings/providers/form/sections.json index ee544ae18..001571216 100644 --- a/messages/ru/settings/providers/form/sections.json +++ b/messages/ru/settings/providers/form/sections.json @@ -101,10 +101,10 @@ }, "title": "Прокси", "url": { - "formats": "Поддерживаемые форматы:", + "formats": "Поддерживаются протоколы http://, https://, socks5://, socks4://. Для прокси с аутентификацией используйте формат http://user:password@host:port (специальные символы в пароле кодируйте URL-кодировкой, например # как %23)", "label": "URL прокси", "optional": "(необязательно)", - "placeholder": "например: http://proxy.example.com:8080 или socks5://127.0.0.1:1080" + "placeholder": "например: http://proxy.example.com:8080 или http://user:pass@proxy:8080" } }, "rateLimit": { diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index 48b29e90c..d250d952c 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -95,6 +95,8 @@ "exporting": "导出中...", "exportSuccess": "导出成功", "exportError": "导出失败", + "exportPreparing": "正在准备导出...", + "exportProgress": "已导出 {current} / {total} 条", "quickFilters": { "today": "今天", "thisWeek": "本周", diff --git a/messages/zh-CN/settings/config.json b/messages/zh-CN/settings/config.json index 53ac66081..36dc00035 100644 --- a/messages/zh-CN/settings/config.json +++ b/messages/zh-CN/settings/config.json @@ -30,6 +30,13 @@ "original": "重定向前(原始模型)", "redirected": "重定向后(实际模型)" }, + "codexPriorityBillingSource": "Codex Priority 计费来源", + "codexPriorityBillingSourcePlaceholder": "选择 Codex Priority 计费来源", + "codexPriorityBillingSourceDesc": "控制 Codex Priority(Fast Mode)单独计费使用哪个 service_tier。默认按 Requested Service Tier 计费;若选择 Actual Service Tier,则优先使用响应返回值,响应未返回时回退到请求值。", + "codexPriorityBillingSourceOptions": { + "requested": "Requested Service Tier(默认)", + "actual": "Actual Service Tier(缺失时回退 Requested)" + }, "allowGlobalView": "允许查看全站使用量", "allowGlobalViewDesc": "关闭后,普通用户在仪表盘仅能查看自己密钥的使用统计。", "verboseProviderError": "详细供应商错误信息", diff --git a/messages/zh-CN/settings/providers/form/sections.json b/messages/zh-CN/settings/providers/form/sections.json index 9303a054e..865bb4080 100644 --- a/messages/zh-CN/settings/providers/form/sections.json +++ b/messages/zh-CN/settings/providers/form/sections.json @@ -351,8 +351,8 @@ "url": { "label": "代理地址", "optional": "(可选)", - "placeholder": "例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080", - "formats": "支持格式:" + "placeholder": "例如: http://proxy.example.com:8080 或 http://user:pass@proxy:8080", + "formats": "支持 http://、https://、socks5://、socks4:// 协议。需要认证时使用 http://user:password@host:port 格式(密码中的特殊字符需 URL 编码,如 # 编码为 %23)" }, "fallback": { "label": "代理失败时降级到直连", diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index 1bbfbdd17..a600f5e43 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -95,6 +95,8 @@ "exporting": "匯出中...", "exportSuccess": "匯出成功", "exportError": "匯出失敗", + "exportPreparing": "正在準備匯出...", + "exportProgress": "已匯出 {current} / {total} 筆", "quickFilters": { "today": "今天", "thisWeek": "本週", diff --git a/messages/zh-TW/settings/config.json b/messages/zh-TW/settings/config.json index d7bdc6e6c..19ee71197 100644 --- a/messages/zh-TW/settings/config.json +++ b/messages/zh-TW/settings/config.json @@ -13,6 +13,13 @@ "redirected": "重新導向後(實際模型)" }, "billingModelSourcePlaceholder": "選擇計費模型來源", + "codexPriorityBillingSource": "Codex Priority 計費來源", + "codexPriorityBillingSourceDesc": "控制 Codex Priority(Fast Mode)單獨計費使用哪個 service_tier。預設按 Requested Service Tier 計費;若選擇 Actual Service Tier,則優先使用回應返回值,回應未返回時回退到請求值。", + "codexPriorityBillingSourceOptions": { + "requested": "Requested Service Tier(預設)", + "actual": "Actual Service Tier(缺失時回退 Requested)" + }, + "codexPriorityBillingSourcePlaceholder": "選擇 Codex Priority 計費來源", "cleanupBatchSize": "批次大小", "cleanupBatchSizeDesc": "每批刪除的記錄數(範圍:1000-100000,建議 10000)", "cleanupBatchSizePlaceholder": "10000", diff --git a/messages/zh-TW/settings/providers/form/sections.json b/messages/zh-TW/settings/providers/form/sections.json index 4493edc42..a85b642a5 100644 --- a/messages/zh-TW/settings/providers/form/sections.json +++ b/messages/zh-TW/settings/providers/form/sections.json @@ -101,10 +101,10 @@ }, "title": "代理設定", "url": { - "formats": "支援格式:", + "formats": "支援 http://、https://、socks5://、socks4:// 協定。需要認證時使用 http://user:password@host:port 格式(密碼中的特殊字元需 URL 編碼,如 # 編碼為 %23)", "label": "代理位址", "optional": "(選填)", - "placeholder": "例如:http://proxy.example.com:8080 或 socks5://127.0.0.1:1080" + "placeholder": "例如:http://proxy.example.com:8080 或 http://user:pass@proxy:8080" } }, "rateLimit": { diff --git a/package.json b/package.json index 519e1c55e..b72b465ba 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,12 @@ "version": "0.6.0", "private": true, "scripts": { - "dev": "next dev --port 13500", - "build": "next build && (node scripts/copy-version-to-standalone.cjs || bun scripts/copy-version-to-standalone.cjs)", + "dev": "tsgo -p tsconfig.json --noEmit && next dev --port 13500", + "build": "tsgo -p tsconfig.json --noEmit && next build && (node scripts/copy-version-to-standalone.cjs || bun scripts/copy-version-to-standalone.cjs)", "start": "next start", "lint": "biome check .", "lint:fix": "biome check --write .", "typecheck": "tsgo -p tsconfig.json --noEmit", - "typecheck:tsc": "tsc -p tsconfig.json --noEmit", "format": "biome format --write .", "format:check": "biome format .", "clean:cache": "rm -rf .next tsconfig.tsbuildinfo node_modules/.cache", @@ -67,7 +66,7 @@ "@radix-ui/react-tabs": "^1", "@radix-ui/react-tooltip": "^1", "@scalar/hono-api-reference": "^0.9", - "@tanstack/react-query": "^5", + "@tanstack/react-query": "^5.94.5", "@tanstack/react-virtual": "^3", "@tanstack/virtual-core": "^3", "agentation": "^1.3.2", @@ -88,7 +87,7 @@ "ioredis": "^5", "jspdf": "^4", "lucide-react": "^0.555", - "next": "^16", + "next": "^16.2.1", "next-intl": "^4", "next-themes": "^0.4", "pino": "^10", @@ -111,7 +110,7 @@ "zod": "^4" }, "devDependencies": { - "@biomejs/biome": "^2", + "@biomejs/biome": "^2.4.8", "@tailwindcss/postcss": "^4", "@types/ioredis": "^5", "@types/node": "^24", @@ -119,7 +118,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "@types/react-syntax-highlighter": "^15.5.13", - "@typescript/native-preview": "7.0.0-dev.20251219.1", + "@typescript/native-preview": "7.0.0-dev.20260321.1", "@vitest/coverage-v8": "^4.0.16", "@vitest/ui": "^4.0.16", "bun-types": "^1", diff --git a/src/actions/keys.ts b/src/actions/keys.ts index 264a6118f..4833bfe7a 100644 --- a/src/actions/keys.ts +++ b/src/actions/keys.ts @@ -607,11 +607,7 @@ export async function removeKey(keyId: number): Promise { for (const k of userKeys) { if (k.id === keyId) continue; const group = k.providerGroup || PROVIDER_GROUP.DEFAULT; - group - .split(",") - .map((g) => g.trim()) - .filter(Boolean) - .forEach((g) => remainingGroups.add(g)); + parseProviderGroups(group).forEach((g) => remainingGroups.add(g)); } const { findUserById } = await import("@/repository/user"); diff --git a/src/actions/my-usage.ts b/src/actions/my-usage.ts index cc7ee0432..d5db9bfe7 100644 --- a/src/actions/my-usage.ts +++ b/src/actions/my-usage.ts @@ -16,9 +16,11 @@ import { LEDGER_BILLING_CONDITION } from "@/repository/_shared/ledger-conditions import { EXCLUDE_WARMUP_CONDITION } from "@/repository/_shared/message-request-conditions"; import { getSystemSettings } from "@/repository/system-config"; import { + findUsageLogsForKeyBatch, findUsageLogsForKeySlim, getDistinctEndpointsForKey, getDistinctModelsForKey, + type UsageLogSlimBatchResult, type UsageLogSummary, } from "@/repository/usage-logs"; import type { BillingModelSource } from "@/types/system-config"; @@ -168,6 +170,27 @@ export interface MyUsageLogEntry { cacheTtlApplied: string | null; } +export interface MyUsageLogsBatchResult { + logs: MyUsageLogEntry[]; + nextCursor: { createdAt: string; id: number } | null; + hasMore: boolean; + currencyCode: CurrencyCode; + billingModelSource: BillingModelSource; +} + +export interface MyUsageLogsFilters { + startDate?: string; + endDate?: string; + sessionId?: string; + model?: string; + statusCode?: number; + excludeStatusCode200?: boolean; + endpoint?: string; + minRetryCount?: number; + page?: number; + pageSize?: number; +} + export interface MyUsageLogsResult { logs: MyUsageLogEntry[]; total: number; @@ -469,7 +492,7 @@ export async function getMyTodayStats(): Promise> { } } -export interface MyUsageLogsFilters { +export interface MyUsageLogsBatchFilters { startDate?: string; endDate?: string; /** Session ID(精确匹配;空字符串/空白视为不筛选) */ @@ -479,8 +502,43 @@ export interface MyUsageLogsFilters { excludeStatusCode200?: boolean; endpoint?: string; minRetryCount?: number; - page?: number; - pageSize?: number; + cursor?: { createdAt: string; id: number }; + limit?: number; +} + +function mapMyUsageLogEntries( + result: Pick, + billingModelSource: BillingModelSource +): MyUsageLogEntry[] { + return result.logs.map((log) => { + const modelRedirect = + log.originalModel && log.model && log.originalModel !== log.model + ? `${log.originalModel} → ${log.model}` + : null; + + const billingModel = + (billingModelSource === "original" ? log.originalModel : log.model) ?? null; + + return { + id: log.id, + createdAt: log.createdAt, + model: log.model, + billingModel, + anthropicEffort: log.anthropicEffort ?? null, + modelRedirect, + inputTokens: log.inputTokens ?? 0, + outputTokens: log.outputTokens ?? 0, + cost: log.costUsd ? Number(log.costUsd) : 0, + statusCode: log.statusCode, + duration: log.durationMs, + endpoint: log.endpoint, + cacheCreationInputTokens: log.cacheCreationInputTokens ?? null, + cacheReadInputTokens: log.cacheReadInputTokens ?? null, + cacheCreation5mInputTokens: log.cacheCreation5mInputTokens ?? null, + cacheCreation1hInputTokens: log.cacheCreation1hInputTokens ?? null, + cacheTtlApplied: log.cacheTtlApplied ?? null, + }; + }); } export async function getMyUsageLogs( @@ -491,17 +549,19 @@ export async function getMyUsageLogs( if (!session) return { ok: false, error: "Unauthorized" }; const settings = await getSystemSettings(); - - const rawPageSize = filters.pageSize && filters.pageSize > 0 ? filters.pageSize : 20; - const pageSize = Math.min(rawPageSize, 100); - const page = filters.page && filters.page > 0 ? filters.page : 1; - const timezone = await resolveSystemTimezone(); const { startTime, endTime } = parseDateRangeInServerTimezone( filters.startDate, filters.endDate, timezone ); + const parsedPageSize = Number(filters.pageSize); + const pageSize = + Number.isFinite(parsedPageSize) && parsedPageSize > 0 + ? Math.min(Math.trunc(parsedPageSize), 100) + : 20; + const parsedPage = Number(filters.page); + const page = Number.isFinite(parsedPage) && parsedPage > 0 ? Math.trunc(parsedPage) : 1; const result = await findUsageLogsForKeySlim({ keyString: session.key.key, sessionId: filters.sessionId, @@ -516,49 +576,64 @@ export async function getMyUsageLogs( pageSize, }); - const logs: MyUsageLogEntry[] = result.logs.map((log) => { - const modelRedirect = - log.originalModel && log.model && log.originalModel !== log.model - ? `${log.originalModel} → ${log.model}` - : null; + return { + ok: true, + data: { + logs: mapMyUsageLogEntries(result, settings.billingModelSource), + total: result.total, + page, + pageSize, + currencyCode: settings.currencyDisplay, + billingModelSource: settings.billingModelSource, + }, + }; + } catch (error) { + logger.error("[my-usage] getMyUsageLogs failed", { error, filters }); + return { ok: false, error: "Failed to get usage logs" }; + } +} - const billingModel = - (settings.billingModelSource === "original" ? log.originalModel : log.model) ?? null; +export async function getMyUsageLogsBatch( + filters: MyUsageLogsBatchFilters = {} +): Promise> { + try { + const session = await getSession({ allowReadOnlyAccess: true }); + if (!session) return { ok: false, error: "Unauthorized" }; - return { - id: log.id, - createdAt: log.createdAt, - model: log.model, - billingModel, - anthropicEffort: log.anthropicEffort ?? null, - modelRedirect, - inputTokens: log.inputTokens ?? 0, - outputTokens: log.outputTokens ?? 0, - cost: log.costUsd ? Number(log.costUsd) : 0, - statusCode: log.statusCode, - duration: log.durationMs, - endpoint: log.endpoint, - cacheCreationInputTokens: log.cacheCreationInputTokens ?? null, - cacheReadInputTokens: log.cacheReadInputTokens ?? null, - cacheCreation5mInputTokens: log.cacheCreation5mInputTokens ?? null, - cacheCreation1hInputTokens: log.cacheCreation1hInputTokens ?? null, - cacheTtlApplied: log.cacheTtlApplied ?? null, - }; + const settings = await getSystemSettings(); + const timezone = await resolveSystemTimezone(); + const { startTime, endTime } = parseDateRangeInServerTimezone( + filters.startDate, + filters.endDate, + timezone + ); + const limit = filters.limit && filters.limit > 0 ? Math.min(filters.limit, 100) : 20; + const result = await findUsageLogsForKeyBatch({ + keyString: session.key.key, + sessionId: filters.sessionId, + startTime, + endTime, + model: filters.model, + statusCode: filters.statusCode, + excludeStatusCode200: filters.excludeStatusCode200, + endpoint: filters.endpoint, + minRetryCount: filters.minRetryCount, + cursor: filters.cursor, + limit, }); return { ok: true, data: { - logs, - total: result.total, - page, - pageSize, + logs: mapMyUsageLogEntries(result, settings.billingModelSource), + nextCursor: result.nextCursor, + hasMore: result.hasMore, currencyCode: settings.currencyDisplay, billingModelSource: settings.billingModelSource, }, }; } catch (error) { - logger.error("[my-usage] getMyUsageLogs failed", error); + logger.error("[my-usage] getMyUsageLogsBatch failed", error); return { ok: false, error: "Failed to get usage logs" }; } } diff --git a/src/actions/providers.ts b/src/actions/providers.ts index ea3a43af9..162c95ae0 100644 --- a/src/actions/providers.ts +++ b/src/actions/providers.ts @@ -44,6 +44,7 @@ import { } from "@/lib/redis/circuit-breaker-config"; import { RedisKVStore } from "@/lib/redis/redis-kv-store"; import { SessionManager } from "@/lib/session-manager"; +import { normalizeProviderGroupTag, parseProviderGroups } from "@/lib/utils/provider-group"; import { maskKey } from "@/lib/utils/validation"; import { extractZodErrorCode, formatZodError } from "@/lib/utils/zod-i18n"; import { validateProviderUrlForConnectivity } from "@/lib/validation/provider-url"; @@ -431,10 +432,7 @@ export async function getAvailableProviderGroups(userId?: number): Promise g.trim()) - .filter(Boolean); + const userGroups = parseProviderGroups(user?.providerGroup || PROVIDER_GROUP.DEFAULT); // 管理员通配符:可访问所有分组 if (userGroups.includes(PROVIDER_GROUP.ALL)) { @@ -462,17 +460,12 @@ export async function getProviderGroupsWithCount(): Promise< const groupCounts = new Map(); for (const provider of providers) { - const groupTag = provider.groupTag?.trim(); - if (!groupTag) { + const groups = parseProviderGroups(provider.groupTag); + if (groups.length === 0) { groupCounts.set(PROVIDER_GROUP.DEFAULT, (groupCounts.get(PROVIDER_GROUP.DEFAULT) || 0) + 1); continue; } - const groups = groupTag - .split(",") - .map((g) => g.trim()) - .filter(Boolean); - for (const group of groups) { groupCounts.set(group, (groupCounts.get(group) || 0) + 1); } @@ -589,6 +582,7 @@ export async function addProvider(data: { const payload = { ...validated, + group_tag: normalizeProviderGroupTag(validated.group_tag), limit_5h_usd: validated.limit_5h_usd ?? null, limit_daily_usd: validated.limit_daily_usd ?? null, daily_reset_mode: validated.daily_reset_mode ?? "fixed", @@ -766,6 +760,9 @@ export async function editProvider( const payload = { ...validated, + ...(validated.group_tag !== undefined && { + group_tag: normalizeProviderGroupTag(validated.group_tag), + }), ...(faviconUrl !== undefined && { favicon_url: faviconUrl }), }; @@ -2235,7 +2232,9 @@ export async function batchUpdateProviders( if (updates.cost_multiplier !== undefined) { repositoryUpdates.costMultiplier = updates.cost_multiplier.toString(); } - if (updates.group_tag !== undefined) repositoryUpdates.groupTag = updates.group_tag; + if (updates.group_tag !== undefined) { + repositoryUpdates.groupTag = normalizeProviderGroupTag(updates.group_tag); + } if (updates.model_redirects !== undefined) { repositoryUpdates.modelRedirects = updates.model_redirects; } @@ -4795,10 +4794,7 @@ async function fetchAnthropicModels( * 解析分组字符串为数组 */ function parseGroupString(groupString: string): string[] { - return groupString - .split(",") - .map((g) => g.trim()) - .filter(Boolean); + return parseProviderGroups(groupString); } /** diff --git a/src/actions/request-filters.ts b/src/actions/request-filters.ts index 4098f34df..a5b322ff4 100644 --- a/src/actions/request-filters.ts +++ b/src/actions/request-filters.ts @@ -6,6 +6,7 @@ import { getSession } from "@/lib/auth"; import { logger } from "@/lib/logger"; import { requestFilterEngine } from "@/lib/request-filter-engine"; import type { FilterMatcher, FilterOperation, InsertOp } from "@/lib/request-filter-types"; +import { parseProviderGroups } from "@/lib/utils/provider-group"; import { createRequestFilter, deleteRequestFilter, @@ -458,7 +459,7 @@ export async function getDistinctProviderGroupsAction(): Promise(); for (const row of result) { if (row.groupTag) { - const tags = row.groupTag.split(",").map((tag) => tag.trim()); + const tags = parseProviderGroups(row.groupTag); for (const tag of tags) { if (tag) allTags.add(tag); } diff --git a/src/actions/system-config.ts b/src/actions/system-config.ts index 4e6f8a1c6..f9f200865 100644 --- a/src/actions/system-config.ts +++ b/src/actions/system-config.ts @@ -8,7 +8,11 @@ import { logger } from "@/lib/logger"; import { resolveSystemTimezone } from "@/lib/utils/timezone"; import { UpdateSystemSettingsSchema } from "@/lib/validation/schemas"; import { getSystemSettings, updateSystemSettings } from "@/repository/system-config"; -import type { ResponseFixerConfig, SystemSettings } from "@/types/system-config"; +import type { + CodexPriorityBillingSource, + ResponseFixerConfig, + SystemSettings, +} from "@/types/system-config"; import type { ActionResult } from "./types"; export async function fetchSystemSettings(): Promise> { @@ -47,6 +51,7 @@ export async function saveSystemSettings(formData: { allowGlobalUsageView?: boolean; currencyDisplay?: string; billingModelSource?: string; + codexPriorityBillingSource?: CodexPriorityBillingSource; timezone?: string | null; enableAutoCleanup?: boolean; cleanupRetentionDays?: number; @@ -84,6 +89,7 @@ export async function saveSystemSettings(formData: { allowGlobalUsageView: validated.allowGlobalUsageView, currencyDisplay: validated.currencyDisplay, billingModelSource: validated.billingModelSource, + codexPriorityBillingSource: validated.codexPriorityBillingSource, timezone: validated.timezone, enableAutoCleanup: validated.enableAutoCleanup, cleanupRetentionDays: validated.cleanupRetentionDays, diff --git a/src/actions/usage-logs.ts b/src/actions/usage-logs.ts index 81e15697c..25a96fc3f 100644 --- a/src/actions/usage-logs.ts +++ b/src/actions/usage-logs.ts @@ -8,6 +8,7 @@ import { } from "@/lib/constants/usage-logs.constants"; import { logger } from "@/lib/logger"; import { readLiveChainBatch } from "@/lib/redis/live-chain-store"; +import { RedisKVStore } from "@/lib/redis/redis-kv-store"; import { getRetryCount } from "@/lib/utils/provider-chain-formatter"; import { isProviderFinalized } from "@/lib/utils/provider-display"; import { @@ -39,6 +40,256 @@ let filterOptionsCache: { expiresAt: number; } | null = null; +const USAGE_LOGS_EXPORT_BATCH_SIZE = 500; +const USAGE_LOGS_EXPORT_JOB_TTL_MS = 15 * 60 * 1000; +const USAGE_LOGS_EXPORT_JOB_TTL_SECONDS = Math.floor(USAGE_LOGS_EXPORT_JOB_TTL_MS / 1000); +const USAGE_LOGS_EXPORT_PROGRESS_UPDATE_INTERVAL_MS = 800; +const CSV_HEADERS = [ + "Time", + "User", + "Key", + "Provider", + "Model", + "Original Model", + "Endpoint", + "Status Code", + "Input Tokens", + "Output Tokens", + "Cache Write 5m", + "Cache Write 1h", + "Cache Read", + "Total Tokens", + "Cost (USD)", + "Duration (ms)", + "Session ID", + "Retry Count", +] as const; + +type UsageLogsSession = NonNullable>>; + +export interface UsageLogsExportStatus { + jobId: string; + status: "queued" | "running" | "completed" | "failed"; + processedRows: number; + totalRows: number; + progressPercent: number; + error?: string; +} + +interface UsageLogsExportJobRecord extends UsageLogsExportStatus { + ownerUserId: number; +} + +const usageLogsExportStatusStore = new RedisKVStore({ + prefix: "cch:usage-logs:export:status:", + defaultTtlSeconds: USAGE_LOGS_EXPORT_JOB_TTL_SECONDS, +}); + +const usageLogsExportCsvStore = new RedisKVStore({ + prefix: "cch:usage-logs:export:csv:", + defaultTtlSeconds: USAGE_LOGS_EXPORT_JOB_TTL_SECONDS, +}); + +function usageLogsExportCsvKey(jobId: string): string { + return `${jobId}:csv`; +} + +function resolveUsageLogFiltersForSession( + session: UsageLogsSession, + filters: Omit +): Omit { + return session.user.role === "admin" ? filters : { ...filters, userId: session.user.id }; +} + +function toUsageLogsExportStatus(job: UsageLogsExportJobRecord): UsageLogsExportStatus { + return { + jobId: job.jobId, + status: job.status, + processedRows: job.processedRows, + totalRows: job.totalRows, + progressPercent: job.progressPercent, + error: job.error, + }; +} + +function getUsageLogsExportJob( + session: UsageLogsSession, + job: UsageLogsExportJobRecord | null, + _jobId: string +): UsageLogsExportJobRecord | null { + if (!job || job.ownerUserId !== session.user.id) { + return null; + } + return job; +} + +function buildCsvRows(logs: UsageLogRow[]): string[] { + return logs.map((log) => { + const retryCount = log.providerChain ? getRetryCount(log.providerChain) : 0; + return [ + log.createdAt ? new Date(log.createdAt).toISOString() : "", + escapeCsvField(log.userName), + escapeCsvField(log.keyName), + escapeCsvField(log.providerName ?? ""), + escapeCsvField(log.model ?? ""), + escapeCsvField(log.originalModel ?? ""), + escapeCsvField(log.endpoint ?? ""), + log.statusCode?.toString() ?? "", + log.inputTokens?.toString() ?? "0", + log.outputTokens?.toString() ?? "0", + log.cacheCreation5mInputTokens?.toString() ?? "0", + log.cacheCreation1hInputTokens?.toString() ?? "0", + log.cacheReadInputTokens?.toString() ?? "0", + log.totalTokens.toString(), + log.costUsd ?? "0", + log.durationMs?.toString() ?? "", + escapeCsvField(log.sessionId ?? ""), + retryCount.toString(), + ].join(","); + }); +} + +function buildUsageLogsExportProgress( + processedRows: number, + totalRows: number, + hasMore: boolean +): Pick { + const effectiveTotalRows = Math.max(totalRows, hasMore ? processedRows + 1 : processedRows); + const progressPercent = + effectiveTotalRows <= 0 + ? 100 + : hasMore + ? Math.min(99, Math.floor((processedRows / effectiveTotalRows) * 100)) + : 100; + + return { + processedRows, + totalRows: effectiveTotalRows, + progressPercent, + }; +} + +async function buildUsageLogsExportCsv( + filters: Omit, + onProgress?: ( + progress: Pick + ) => Promise | void +): Promise { + const initialResult = await findUsageLogsWithDetails({ ...filters, page: 1, pageSize: 1 }); + let estimatedTotalRows = initialResult.total; + + if (estimatedTotalRows === 0) { + const stats = await findUsageLogsStats(filters); + estimatedTotalRows = stats.totalRequests; + } + + const csvLines = [CSV_HEADERS.join(",")]; + let cursor: UsageLogBatchFilters["cursor"] | undefined; + let processedRows = 0; + + while (true) { + const batch = await findUsageLogsBatch({ + ...filters, + cursor, + limit: USAGE_LOGS_EXPORT_BATCH_SIZE, + }); + + if (batch.logs.length > 0) { + csvLines.push(...buildCsvRows(batch.logs)); + processedRows += batch.logs.length; + } + + const progress = buildUsageLogsExportProgress(processedRows, estimatedTotalRows, batch.hasMore); + estimatedTotalRows = progress.totalRows; + await onProgress?.(progress); + + if (!batch.hasMore || !batch.nextCursor) { + break; + } + + cursor = batch.nextCursor; + } + + return `\uFEFF${csvLines.join("\n")}`; +} + +async function runUsageLogsExportJob( + jobId: string, + filters: Omit +): Promise { + const existingJob = await usageLogsExportStatusStore.get(jobId); + if (!existingJob) { + return; + } + + await usageLogsExportStatusStore.set(jobId, { + ...existingJob, + status: "running", + error: undefined, + }); + + try { + let lastProgressUpdateAt = 0; + const csv = await buildUsageLogsExportCsv(filters, async (progress) => { + const now = Date.now(); + if ( + progress.progressPercent < 100 && + now - lastProgressUpdateAt < USAGE_LOGS_EXPORT_PROGRESS_UPDATE_INTERVAL_MS + ) { + return; + } + lastProgressUpdateAt = now; + + const currentJob = await usageLogsExportStatusStore.get(jobId); + if (!currentJob) { + return; + } + + await usageLogsExportStatusStore.set(jobId, { + ...currentJob, + status: "running", + ...progress, + }); + }); + + const currentJob = await usageLogsExportStatusStore.get(jobId); + if (!currentJob) { + return; + } + + const csvStored = await usageLogsExportCsvStore.set(usageLogsExportCsvKey(jobId), csv); + if (!csvStored) { + await usageLogsExportStatusStore.set(jobId, { + ...currentJob, + status: "failed", + progressPercent: 0, + error: "Failed to persist CSV to Redis", + }); + return; + } + + await usageLogsExportStatusStore.set(jobId, { + ...currentJob, + status: "completed", + progressPercent: 100, + error: undefined, + }); + } catch (error) { + logger.error("Failed to run usage logs export job:", error); + const currentJob = await usageLogsExportStatusStore.get(jobId); + if (!currentJob) { + return; + } + + await usageLogsExportStatusStore.set(jobId, { + ...currentJob, + status: "failed", + progressPercent: 0, + error: error instanceof Error ? error.message : "Export failed", + }); + } +} + /** * 获取使用日志(根据权限过滤) */ @@ -77,16 +328,8 @@ export async function exportUsageLogs( return { ok: false, error: "未登录" }; } - // 如果不是 admin,强制过滤为当前用户 - const finalFilters: UsageLogFilters = - session.user.role === "admin" - ? { ...filters, page: 1, pageSize: 10000 } - : { ...filters, userId: session.user.id, page: 1, pageSize: 10000 }; - - const result = await findUsageLogsWithDetails(finalFilters); - - // 生成 CSV - const csv = generateCsv(result.logs); + const finalFilters = resolveUsageLogFiltersForSession(session, filters); + const csv = await buildUsageLogsExportCsv(finalFilters); return { ok: true, data: csv }; } catch (error) { @@ -96,74 +339,117 @@ export async function exportUsageLogs( } } -/** - * 生成 CSV 字符串 - */ -function generateCsv(logs: UsageLogRow[]): string { - const headers = [ - "Time", - "User", - "Key", - "Provider", - "Model", - "Original Model", - "Endpoint", - "Status Code", - "Input Tokens", - "Output Tokens", - "Cache Write 5m", - "Cache Write 1h", - "Cache Read", - "Total Tokens", - "Cost (USD)", - "Duration (ms)", - "Session ID", - "Retry Count", - ]; - - const rows = logs.map((log) => { - const retryCount = log.providerChain ? getRetryCount(log.providerChain) : 0; - return [ - log.createdAt ? new Date(log.createdAt).toISOString() : "", - escapeCsvField(log.userName), - escapeCsvField(log.keyName), - escapeCsvField(log.providerName ?? ""), - escapeCsvField(log.model ?? ""), - escapeCsvField(log.originalModel ?? ""), - escapeCsvField(log.endpoint ?? ""), - log.statusCode?.toString() ?? "", - log.inputTokens?.toString() ?? "0", - log.outputTokens?.toString() ?? "0", - log.cacheCreation5mInputTokens?.toString() ?? "0", - log.cacheCreation1hInputTokens?.toString() ?? "0", - log.cacheReadInputTokens?.toString() ?? "0", - log.totalTokens.toString(), - log.costUsd ?? "0", - log.durationMs?.toString() ?? "", - escapeCsvField(log.sessionId ?? ""), - retryCount.toString(), - ]; - }); +export async function startUsageLogsExport( + filters: Omit +): Promise> { + try { + const session = await getSession(); + if (!session) { + return { ok: false, error: "未登录" }; + } + + const jobId = crypto.randomUUID(); + const finalFilters = resolveUsageLogFiltersForSession(session, filters); + + const stored = await usageLogsExportStatusStore.set(jobId, { + jobId, + ownerUserId: session.user.id, + status: "queued", + processedRows: 0, + totalRows: 0, + progressPercent: 0, + }); + + if (!stored) { + return { ok: false, error: "Export job initialization failed" }; + } + + // Defer to next tick so the action returns the jobId immediately. + // Safe for self-hosted Bun server (long-lived process); NOT suitable for serverless. + setTimeout(() => { + void runUsageLogsExportJob(jobId, finalFilters); + }, 0); + + return { ok: true, data: { jobId } }; + } catch (error) { + logger.error("Failed to start usage logs export:", error); + const message = error instanceof Error ? error.message : "Failed to start export"; + return { ok: false, error: message }; + } +} - // 添加 BOM 以支持 Excel 正确识别 UTF-8 - const bom = "\uFEFF"; - const csvContent = [headers.join(","), ...rows.map((row) => row.join(","))].join("\n"); +export async function getUsageLogsExportStatus( + jobId: string +): Promise> { + try { + const session = await getSession(); + if (!session) { + return { ok: false, error: "未登录" }; + } + + const job = getUsageLogsExportJob(session, await usageLogsExportStatusStore.get(jobId), jobId); + if (!job) { + return { ok: false, error: "Export job not found or expired" }; + } + + return { ok: true, data: toUsageLogsExportStatus(job) }; + } catch (error) { + logger.error("Failed to get usage logs export status:", error); + const message = error instanceof Error ? error.message : "Failed to get export status"; + return { ok: false, error: message }; + } +} - return bom + csvContent; +export async function downloadUsageLogsExport(jobId: string): Promise> { + try { + const session = await getSession(); + if (!session) { + return { ok: false, error: "未登录" }; + } + + const job = getUsageLogsExportJob(session, await usageLogsExportStatusStore.get(jobId), jobId); + if (!job) { + return { ok: false, error: "Export job not found or expired" }; + } + + if (job.status === "failed") { + return { ok: false, error: job.error || "Export failed" }; + } + + if (job.status !== "completed") { + return { ok: false, error: "Export not yet completed" }; + } + + const csv = await usageLogsExportCsvStore.get(usageLogsExportCsvKey(jobId)); + if (!csv) { + return { ok: false, error: "Export file not found or expired" }; + } + + return { ok: true, data: csv }; + } catch (error) { + logger.error("Failed to download usage logs export:", error); + const message = error instanceof Error ? error.message : "Failed to download export"; + return { ok: false, error: message }; + } } /** * 转义 CSV 字段(防止 CSV 公式注入攻击) */ function escapeCsvField(field: string): string { - // Prevent CSV formula injection by prefixing dangerous characters - const dangerousChars = ["=", "+", "-", "@", "\t", "\r"]; + const dangerousChars = ["=", "+", "-", "@"]; + const trimmedField = field.trimStart(); let safeField = field; - if (dangerousChars.some((char) => field.startsWith(char))) { - safeField = `'${field}`; // Prefix with single quote to prevent formula execution + if (trimmedField && dangerousChars.some((char) => trimmedField.startsWith(char))) { + safeField = `'${field}`; } - if (safeField.includes(",") || safeField.includes('"') || safeField.includes("\n")) { + if ( + safeField.includes(",") || + safeField.includes('"') || + safeField.includes("\n") || + safeField.includes("\r") + ) { return `"${safeField.replace(/"/g, '""')}"`; } return safeField; diff --git a/src/actions/users.ts b/src/actions/users.ts index 58e1296c8..4f782ac8f 100644 --- a/src/actions/users.ts +++ b/src/actions/users.ts @@ -13,7 +13,7 @@ import { getUnauthorizedFields } from "@/lib/permissions/user-field-permissions" import { invalidateCachedUser } from "@/lib/security/api-key-auth-cache"; import { parseDateInputAsTimezone } from "@/lib/utils/date-input"; import { ERROR_CODES } from "@/lib/utils/error-messages"; -import { normalizeProviderGroup } from "@/lib/utils/provider-group"; +import { normalizeProviderGroup, parseProviderGroups } from "@/lib/utils/provider-group"; import { resolveSystemTimezone } from "@/lib/utils/timezone"; import { maskKey } from "@/lib/utils/validation"; import { formatZodError } from "@/lib/utils/zod-i18n"; @@ -29,7 +29,6 @@ import { createUser, deleteUser, findUserById, - findUserList, findUserListBatch, getAllUserProviderGroups as getAllUserProviderGroupsRepository, getAllUserTags as getAllUserTagsRepository, @@ -47,6 +46,10 @@ export interface GetUsersBatchParams { cursor?: string; limit?: number; searchTerm?: string; + query?: string; + keyword?: string; + page?: number; + offset?: number; tagFilters?: string[]; keyGroupFilters?: string[]; statusFilter?: "all" | "active" | "expired" | "expiringSoon" | "enabled" | "disabled"; @@ -63,6 +66,122 @@ export interface GetUsersBatchParams { sortOrder?: "asc" | "desc"; } +const USER_LIST_DEFAULT_LIMIT = 50; +const USER_LIST_MAX_LIMIT = 200; +const SEARCH_USERS_MAX_LIMIT = 5000; + +type UserActionSession = { + user: { id: number }; + key: { canLoginWebUi: boolean }; +}; + +function normalizeLegacySearchTerm(params?: GetUsersBatchParams): string | undefined { + for (const candidate of [params?.searchTerm, params?.query, params?.keyword]) { + const trimmed = candidate?.trim(); + if (trimmed) { + return trimmed; + } + } + + return undefined; +} + +function normalizeUserListParams(params?: GetUsersBatchParams): GetUsersBatchParams { + const limit = + typeof params?.limit === "number" && Number.isFinite(params.limit) && params.limit > 0 + ? Math.min(Math.trunc(params.limit), USER_LIST_MAX_LIMIT) + : undefined; + + let cursor = params?.cursor?.trim() || undefined; + if (!cursor) { + const offset = + typeof params?.offset === "number" && Number.isFinite(params.offset) + ? Math.max(0, Math.trunc(params.offset)) + : undefined; + const page = + typeof params?.page === "number" && Number.isFinite(params.page) + ? Math.max(0, Math.trunc(params.page)) + : undefined; + + if (offset !== undefined) { + cursor = String(offset); + } else if (page !== undefined) { + const effectiveLimit = limit ?? USER_LIST_DEFAULT_LIMIT; + cursor = String(Math.max(page - 1, 0) * effectiveLimit); + } + } + + return { + cursor, + limit, + searchTerm: normalizeLegacySearchTerm(params), + tagFilters: params?.tagFilters, + keyGroupFilters: params?.keyGroupFilters, + statusFilter: params?.statusFilter, + sortBy: params?.sortBy, + sortOrder: params?.sortOrder, + }; +} + +function hasExplicitPaginationParams( + params?: GetUsersBatchParams, + normalizedParams = normalizeUserListParams(params) +): boolean { + return Boolean( + normalizedParams.cursor !== undefined || + normalizedParams.limit !== undefined || + params?.page !== undefined || + params?.offset !== undefined + ); +} + +function hasSearchOrFilterOverrides(normalizedParams: GetUsersBatchParams): boolean { + return Boolean( + normalizedParams.searchTerm || + (normalizedParams.tagFilters?.length ?? 0) > 0 || + (normalizedParams.keyGroupFilters?.length ?? 0) > 0 || + normalizedParams.statusFilter || + normalizedParams.sortBy || + normalizedParams.sortOrder + ); +} + +async function loadAllUsersForAdmin(baseParams?: GetUsersBatchParams): Promise { + const users: User[] = []; + const normalizedBaseParams = normalizeUserListParams(baseParams); + let cursor = normalizedBaseParams.cursor; + + while (true) { + const page = await findUserListBatch({ + ...normalizedBaseParams, + cursor, + limit: USER_LIST_MAX_LIMIT, + }); + + users.push(...page.users); + + if (!page.hasMore || !page.nextCursor) { + return users; + } + + cursor = page.nextCursor; + } +} + +function normalizeSearchUsersLimit(limit?: number): number | undefined { + if (limit === undefined) return undefined; + if (!Number.isFinite(limit)) return SEARCH_USERS_MAX_LIMIT; + return Math.min(Math.max(1, Math.trunc(limit)), SEARCH_USERS_MAX_LIMIT); +} + +function canExposeFullKey( + session: UserActionSession, + targetUser: Pick, + isAdmin: boolean +): boolean { + return session.key.canLoginWebUi && (isAdmin || session.user.id === targetUser.id); +} + /** * 批量获取用户列表的返回结果。 */ @@ -192,11 +311,7 @@ export async function syncUserProviderGroupFromKeys(userId: number): Promise g.trim()) - .filter(Boolean) - .forEach((g) => allGroups.add(g)); + parseProviderGroups(group).forEach((g) => allGroups.add(g)); } const newProviderGroup = @@ -208,7 +323,7 @@ export async function syncUserProviderGroupFromKeys(userId: number): Promise { +export async function getUsers(params?: GetUsersBatchParams): Promise { try { const session = await getSession(); if (!session) { @@ -221,11 +336,18 @@ export async function getUsers(): Promise { // Treat any non-admin role as non-admin for safety. const isAdmin = session.user.role === "admin"; + const normalizedParams = normalizeUserListParams(params); // 非 admin 用户只能看到自己的数据(从 DB 获取完整用户信息) let users: User[] = []; if (isAdmin) { - users = await findUserList(); // 管理员可以看到所有用户 + if (hasExplicitPaginationParams(params, normalizedParams)) { + users = (await findUserListBatch(normalizedParams)).users; + } else if (hasSearchOrFilterOverrides(normalizedParams)) { + users = await loadAllUsersForAdmin(normalizedParams); + } else { + users = await loadAllUsersForAdmin(); + } } else { const selfUser = await findUserById(session.user.id); users = selfUser ? [selfUser] : []; @@ -285,8 +407,7 @@ export async function getUsers(): Promise { allowedModels: user.allowedModels ?? [], keys: keys.map((key) => { const stats = statisticsLookup.get(key.id); - // 用户可以查看和复制自己的密钥,管理员可以查看和复制所有密钥 - const canUserManageKey = isAdmin || session.user.id === user.id; + const canUserManageKey = canExposeFullKey(session, user, isAdmin); return { id: key.id, name: key.name, @@ -365,7 +486,8 @@ export async function getUsers(): Promise { } export async function searchUsersForFilter( - searchTerm?: string + searchTerm?: string, + limit?: number ): Promise>> { try { const tError = await getTranslations("errors"); @@ -387,7 +509,10 @@ export async function searchUsersForFilter( }; } - const users = await searchUsersForFilterRepository(searchTerm); + const users = await searchUsersForFilterRepository( + searchTerm, + normalizeSearchUsersLimit(limit) + ); return { ok: true, data: users }; } catch (error) { logger.error("Failed to search users for filter:", error); @@ -396,6 +521,13 @@ export async function searchUsersForFilter( } } +export async function searchUsers( + searchTerm?: string, + limit?: number +): Promise>> { + return searchUsersForFilter(searchTerm, limit); +} + /** * 获取所有用户标签(用于标签筛选下拉框) * 返回所有用户的标签,不受当前筛选条件影响 @@ -499,16 +631,8 @@ export async function getUsersBatch( const locale = await getLocale(); const t = await getTranslations("users"); - const { users, nextCursor, hasMore } = await findUserListBatch({ - cursor: params.cursor, - limit: params.limit, - searchTerm: params.searchTerm, - tagFilters: params.tagFilters, - keyGroupFilters: params.keyGroupFilters, - statusFilter: params.statusFilter, - sortBy: params.sortBy, - sortOrder: params.sortOrder, - }); + const normalizedParams = normalizeUserListParams(params); + const { users, nextCursor, hasMore } = await findUserListBatch(normalizedParams); if (users.length === 0) { return { ok: true, data: { users: [], nextCursor, hasMore } }; @@ -526,6 +650,7 @@ export async function getUsersBatch( const keys = keysMap.get(user.id) || []; const usageRecords = usageMap.get(user.id) || []; const keyStatistics = statisticsMap.get(user.id) || []; + const canUserManageKey = canExposeFullKey(session, user, true); const usageLookup = new Map( usageRecords.map((item) => [ @@ -563,8 +688,8 @@ export async function getUsersBatch( id: key.id, name: key.name, maskedKey: maskKey(key.key), - fullKey: key.key, - canCopy: true, + fullKey: canUserManageKey ? key.key : undefined, + canCopy: canUserManageKey, expiresAt: key.expiresAt ? key.expiresAt.toISOString().split("T")[0] : t("neverExpires"), @@ -667,16 +792,8 @@ export async function getUsersBatchCore( const locale = await getLocale(); const t = await getTranslations("users"); - const { users, nextCursor, hasMore } = await findUserListBatch({ - cursor: params.cursor, - limit: params.limit, - searchTerm: params.searchTerm, - tagFilters: params.tagFilters, - keyGroupFilters: params.keyGroupFilters, - statusFilter: params.statusFilter, - sortBy: params.sortBy, - sortOrder: params.sortOrder, - }); + const normalizedParams = normalizeUserListParams(params); + const { users, nextCursor, hasMore } = await findUserListBatch(normalizedParams); if (users.length === 0) { return { ok: true, data: { users: [], nextCursor, hasMore } }; @@ -687,6 +804,7 @@ export async function getUsersBatchCore( const userDisplays: UserDisplay[] = users.map((user) => { const keys = keysMap.get(user.id) || []; + const canUserManageKey = canExposeFullKey(session, user, true); return { id: user.id, @@ -714,8 +832,8 @@ export async function getUsersBatchCore( id: key.id, name: key.name, maskedKey: maskKey(key.key), - fullKey: key.key, - canCopy: true, + fullKey: canUserManageKey ? key.key : undefined, + canCopy: canUserManageKey, expiresAt: key.expiresAt ? key.expiresAt.toISOString().split("T")[0] : t("neverExpires"), status: key.isEnabled ? "enabled" : ("disabled" as const), createdAt: key.createdAt, diff --git a/src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx b/src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx index b7f04554a..826098fb0 100644 --- a/src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx +++ b/src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx @@ -3,7 +3,7 @@ import { ArrowUpDown } from "lucide-react"; import { useLocale, useTranslations } from "next-intl"; import * as React from "react"; -import { getUsers } from "@/actions/users"; +import { searchUsers } from "@/actions/users"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { @@ -28,18 +28,41 @@ type SortDirection = "asc" | "desc"; */ export function RateLimitTopUsers({ data }: RateLimitTopUsersProps) { const t = useTranslations("dashboard.rateLimits.topUsers"); + const tRateLimits = useTranslations("dashboard.rateLimits"); const locale = useLocale(); const [users, setUsers] = React.useState>([]); const [loading, setLoading] = React.useState(true); + const [loadError, setLoadError] = React.useState(false); const [sortField, setSortField] = React.useState("count"); const [sortDirection, setSortDirection] = React.useState("desc"); // 加载用户详情 React.useEffect(() => { - getUsers().then((userList) => { - setUsers(userList); - setLoading(false); - }); + let cancelled = false; + + void searchUsers(undefined, 5000) + .then((result) => { + if (!cancelled) { + setUsers(result.ok ? result.data : []); + setLoadError(!result.ok); + } + }) + .catch((error) => { + console.error("RateLimitTopUsers: failed to load users", error); + if (!cancelled) { + setUsers([]); + setLoadError(true); + } + }) + .finally(() => { + if (!cancelled) { + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; }, []); // 组合数据:用户信息 + 事件计数 @@ -92,10 +115,13 @@ export function RateLimitTopUsers({ data }: RateLimitTopUsersProps) { ) : tableData.length === 0 ? (
- {t("noData")} + {loadError ? tRateLimits("error") : t("noData")}
) : (
+ {loadError ? ( +
{tRateLimits("error")}
+ ) : null} diff --git a/src/app/[locale]/dashboard/_components/user/forms/add-key-form.tsx b/src/app/[locale]/dashboard/_components/user/forms/add-key-form.tsx index f0f3bd00a..0754aa389 100644 --- a/src/app/[locale]/dashboard/_components/user/forms/add-key-form.tsx +++ b/src/app/[locale]/dashboard/_components/user/forms/add-key-form.tsx @@ -20,6 +20,7 @@ import { Switch } from "@/components/ui/switch"; import { PROVIDER_GROUP } from "@/lib/constants/provider.constants"; import { useZodForm } from "@/lib/hooks/use-zod-form"; import { getErrorMessage } from "@/lib/utils/error-messages"; +import { parseProviderGroups } from "@/lib/utils/provider-group"; import { KeyFormSchema } from "@/lib/validation/schemas"; import type { KeyDialogUserContext } from "@/types/user"; @@ -125,16 +126,12 @@ export function AddKeyForm({ userId, user, isAdmin = false, onSuccess }: AddKeyF // 选择分组时,自动移除 default(当有多个分组时) const handleProviderGroupChange = useCallback( (newValue: string) => { - const groups = newValue - .split(",") - .map((g) => g.trim()) - .filter(Boolean); - if (groups.length > 1 && groups.includes(PROVIDER_GROUP.DEFAULT)) { - const withoutDefault = groups.filter((g) => g !== PROVIDER_GROUP.DEFAULT); - form.setValue("providerGroup", withoutDefault.join(",")); - } else { - form.setValue("providerGroup", newValue); - } + const groups = parseProviderGroups(newValue); + const normalizedGroups = + groups.length > 1 && groups.includes(PROVIDER_GROUP.DEFAULT) + ? groups.filter((g) => g !== PROVIDER_GROUP.DEFAULT) + : groups; + form.setValue("providerGroup", normalizedGroups.join(",")); }, [form] ); @@ -206,6 +203,7 @@ export function AddKeyForm({ userId, user, isAdmin = false, onSuccess }: AddKeyF : t("providerGroup.description") } suggestions={providerGroupSuggestions} + validateTag={() => true} onInvalidTag={(_tag, reason) => { const messages: Record = { empty: tUI("emptyTag"), diff --git a/src/app/[locale]/dashboard/_components/user/forms/edit-key-form.tsx b/src/app/[locale]/dashboard/_components/user/forms/edit-key-form.tsx index 03feeda54..6251550b5 100644 --- a/src/app/[locale]/dashboard/_components/user/forms/edit-key-form.tsx +++ b/src/app/[locale]/dashboard/_components/user/forms/edit-key-form.tsx @@ -34,6 +34,7 @@ import { Switch } from "@/components/ui/switch"; import { PROVIDER_GROUP } from "@/lib/constants/provider.constants"; import { useZodForm } from "@/lib/hooks/use-zod-form"; import { getErrorMessage } from "@/lib/utils/error-messages"; +import { parseProviderGroups } from "@/lib/utils/provider-group"; import { KeyFormSchema } from "@/lib/validation/schemas"; import type { KeyDialogUserContext } from "@/types/user"; @@ -183,16 +184,12 @@ export function EditKeyForm({ keyData, user, isAdmin = false, onSuccess }: EditK // 选择分组时,自动移除 default(当有多个分组时) const handleProviderGroupChange = useCallback( (newValue: string) => { - const groups = newValue - .split(",") - .map((g) => g.trim()) - .filter(Boolean); - if (groups.length > 1 && groups.includes(PROVIDER_GROUP.DEFAULT)) { - const withoutDefault = groups.filter((g) => g !== PROVIDER_GROUP.DEFAULT); - form.setValue("providerGroup", withoutDefault.join(",")); - } else { - form.setValue("providerGroup", newValue); - } + const groups = parseProviderGroups(newValue); + const normalizedGroups = + groups.length > 1 && groups.includes(PROVIDER_GROUP.DEFAULT) + ? groups.filter((g) => g !== PROVIDER_GROUP.DEFAULT) + : groups; + form.setValue("providerGroup", normalizedGroups.join(",")); }, [form] ); @@ -264,6 +261,8 @@ export function EditKeyForm({ keyData, user, isAdmin = false, onSuccess }: EditK : t("providerGroup.description") } suggestions={providerGroupSuggestions} + // Provider groups intentionally allow shared parser semantics without extra format restrictions. + validateTag={() => true} onInvalidTag={(_tag, reason) => { const messages: Record = { empty: tUI("emptyTag"), diff --git a/src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx b/src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx index 5da461294..247bbb287 100644 --- a/src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx +++ b/src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx @@ -19,6 +19,7 @@ import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { PROVIDER_GROUP } from "@/lib/constants/provider.constants"; import { cn } from "@/lib/utils"; +import { parseProviderGroups } from "@/lib/utils/provider-group"; import { type DailyResetMode, LimitRulePicker, type LimitType } from "./limit-rule-picker"; import { type LimitRuleDisplayItem, LimitRulesDisplay } from "./limit-rules-display"; import { ProviderGroupSelect } from "./provider-group-select"; @@ -125,10 +126,7 @@ function formatDateInput(date?: Date | null): string { } function normalizeGroupList(value?: string | null): string { - const groups = (value ?? "") - .split(",") - .map((g) => g.trim()) - .filter(Boolean); + const groups = parseProviderGroups(value); if (groups.length === 0) return PROVIDER_GROUP.DEFAULT; return Array.from(new Set(groups)).sort().join(","); } @@ -292,7 +290,7 @@ export function KeyEditSection({ [userProviderGroup] ); const userGroups = useMemo( - () => (normalizedUserProviderGroup ? normalizedUserProviderGroup.split(",") : []), + () => parseProviderGroups(normalizedUserProviderGroup), [normalizedUserProviderGroup] ); const normalizedKeyProviderGroup = useMemo( @@ -301,7 +299,7 @@ export function KeyEditSection({ ); const keyGroupOptions = useMemo(() => { if (!normalizedKeyProviderGroup) return []; - return normalizedKeyProviderGroup.split(",").filter(Boolean); + return parseProviderGroups(normalizedKeyProviderGroup); }, [normalizedKeyProviderGroup]); const _extraKeyGroupOption = useMemo(() => { if (!normalizedKeyProviderGroup) return null; @@ -313,10 +311,7 @@ export function KeyEditSection({ // 普通用户选择分组时,自动移除 default const handleUserProviderGroupChange = useCallback( (newValue: string) => { - const groups = newValue - .split(",") - .map((g) => g.trim()) - .filter(Boolean); + const groups = parseProviderGroups(newValue); // 如果有多个分组且包含 default,移除 default if (groups.length > 1 && groups.includes(PROVIDER_GROUP.DEFAULT)) { const withoutDefault = groups.filter((g) => g !== PROVIDER_GROUP.DEFAULT); @@ -507,6 +502,7 @@ export function KeyEditSection({ suggestions={userGroups} maxTags={userGroups.length + 1} maxTagLength={50} + validateTag={() => true} description={ translations.fields.providerGroup.selectHint || "选择此 Key 可使用的供应商分组" } diff --git a/src/app/[locale]/dashboard/_components/user/forms/provider-group-select.tsx b/src/app/[locale]/dashboard/_components/user/forms/provider-group-select.tsx index 37af648a6..e3bd4c813 100644 --- a/src/app/[locale]/dashboard/_components/user/forms/provider-group-select.tsx +++ b/src/app/[locale]/dashboard/_components/user/forms/provider-group-select.tsx @@ -6,6 +6,7 @@ import { getProviderGroupsWithCount } from "@/actions/providers"; import { TagInputField } from "@/components/form/form-field"; import type { TagInputSuggestion } from "@/components/ui/tag-input"; import { PROVIDER_GROUP } from "@/lib/constants/provider.constants"; +import { parseProviderGroups } from "@/lib/utils/provider-group"; export interface ProviderGroupSelectProps { /** Comma-separated group tags. */ @@ -107,10 +108,7 @@ export function ProviderGroupSelect({ // 选择新分组后自动移除 "default" const handleChange = useCallback( (newValue: string) => { - const groupList = newValue - .split(",") - .map((g) => g.trim()) - .filter(Boolean); + const groupList = parseProviderGroups(newValue); // 如果有多个分组且包含 default,移除 default if (groupList.length > 1 && groupList.includes(PROVIDER_GROUP.DEFAULT)) { const withoutDefault = groupList.filter((g) => g !== PROVIDER_GROUP.DEFAULT); @@ -131,6 +129,7 @@ export function ProviderGroupSelect({ maxTags={20} suggestions={suggestions} disabled={disabled} + validateTag={() => true} onInvalidTag={(_tag, reason) => { toast.error(getTranslation(translations, `tagInputErrors.${reason}`, reason)); }} diff --git a/src/app/[locale]/dashboard/_components/user/forms/user-form.tsx b/src/app/[locale]/dashboard/_components/user/forms/user-form.tsx index e8f8a5be7..db0289ee8 100644 --- a/src/app/[locale]/dashboard/_components/user/forms/user-form.tsx +++ b/src/app/[locale]/dashboard/_components/user/forms/user-form.tsx @@ -217,6 +217,8 @@ export function UserForm({ user, onSuccess, currentUser }: UserFormProps) { placeholder={tForm("providerGroup.placeholder")} description={tForm("providerGroup.description")} suggestions={providerGroupSuggestions} + // Provider groups intentionally accept shared parser output without extra format validation. + validateTag={() => true} onInvalidTag={(_tag, reason) => { const messages: Record = { empty: tUI("emptyTag"), diff --git a/src/app/[locale]/dashboard/_components/user/key-row-item.tsx b/src/app/[locale]/dashboard/_components/user/key-row-item.tsx index 2c7a7fd97..0e5a908b0 100644 --- a/src/app/[locale]/dashboard/_components/user/key-row-item.tsx +++ b/src/app/[locale]/dashboard/_components/user/key-row-item.tsx @@ -35,6 +35,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip import { cn } from "@/lib/utils"; import { CURRENCY_CONFIG, type CurrencyCode, formatCurrency } from "@/lib/utils/currency"; import { formatDate } from "@/lib/utils/date-format"; +import { parseProviderGroups } from "@/lib/utils/provider-group"; import { formatTokenAmount } from "@/lib/utils/token"; import { type QuickRenewKey, QuickRenewKeyDialog } from "./forms/quick-renew-key-dialog"; import { KeyFullDisplayDialog } from "./key-full-display-dialog"; @@ -113,10 +114,7 @@ export interface KeyRowItemProps { const EXPIRING_SOON_MS = 72 * 60 * 60 * 1000; // 72小时 function splitGroups(value?: string | null): string[] { - return (value ?? "") - .split(",") - .map((g) => g.trim()) - .filter(Boolean); + return parseProviderGroups(value); } function formatExpiry(expiresAt: string | null | undefined, locale: string): string { diff --git a/src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx b/src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx index cfbf40711..9d113916a 100644 --- a/src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx +++ b/src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx @@ -26,6 +26,7 @@ import { cn } from "@/lib/utils"; import { getContrastTextColor, getGroupColor } from "@/lib/utils/color"; import { getCurrencySymbol } from "@/lib/utils/currency"; import { formatDate } from "@/lib/utils/date-format"; +import { parseProviderGroups } from "@/lib/utils/provider-group"; import type { UserDisplay } from "@/types/user"; import { EditKeyDialog } from "./edit-key-dialog"; import { KeyRowItem } from "./key-row-item"; @@ -86,10 +87,7 @@ const EXPIRING_SOON_MS = 72 * 60 * 60 * 1000; // 72小时 const MAX_VISIBLE_GROUPS = 2; // 最多显示的分组数量 function splitGroups(value?: string | null): string[] { - return (value ?? "") - .split(",") - .map((g) => g.trim()) - .filter(Boolean); + return parseProviderGroups(value); } function getExpiryStatus( diff --git a/src/app/[locale]/dashboard/_components/user/utils/provider-group.ts b/src/app/[locale]/dashboard/_components/user/utils/provider-group.ts index c1d5fde4f..90dc9b54b 100644 --- a/src/app/[locale]/dashboard/_components/user/utils/provider-group.ts +++ b/src/app/[locale]/dashboard/_components/user/utils/provider-group.ts @@ -1,23 +1 @@ -import { PROVIDER_GROUP } from "@/lib/constants/provider.constants"; - -/** - * Normalize provider group value to a consistent format. - * - Trims whitespace - * - Splits by comma and deduplicates - * - Sorts alphabetically - * - Returns DEFAULT if empty or invalid - */ -export function normalizeProviderGroup(value: unknown): string { - if (value === null || value === undefined) return PROVIDER_GROUP.DEFAULT; - if (typeof value !== "string") return PROVIDER_GROUP.DEFAULT; - const trimmed = value.trim(); - if (trimmed === "") return PROVIDER_GROUP.DEFAULT; - - const groups = trimmed - .split(",") - .map((g) => g.trim()) - .filter(Boolean); - if (groups.length === 0) return PROVIDER_GROUP.DEFAULT; - - return Array.from(new Set(groups)).sort().join(","); -} +export { normalizeProviderGroup } from "@/lib/utils/provider-group"; diff --git a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx index 633e617ba..9bb9a2ca9 100644 --- a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx +++ b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx @@ -24,6 +24,7 @@ import { isActualRequest, isHedgeRace, } from "@/lib/utils/provider-chain-formatter"; +import { parseProviderGroups } from "@/lib/utils/provider-group"; import type { ProviderChainItem } from "@/types/message"; import { getFake200ReasonKey } from "./fake200-reason"; @@ -37,16 +38,7 @@ interface ProviderChainPopoverProps { } function parseGroupTags(groupTag?: string | null): string[] { - if (!groupTag) return []; - const seen = new Set(); - const groups: string[] = []; - for (const raw of groupTag.split(",")) { - const trimmed = raw.trim(); - if (!trimmed || seen.has(trimmed)) continue; - seen.add(trimmed); - groups.push(trimmed); - } - return groups; + return Array.from(new Set(parseProviderGroups(groupTag))); } /** diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx index c42820b0d..9f1e70f89 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx @@ -3,10 +3,17 @@ import { format, startOfDay, startOfWeek } from "date-fns"; import { Clock, Download, Network, Server, User } from "lucide-react"; import { useTranslations } from "next-intl"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; -import { exportUsageLogs } from "@/actions/usage-logs"; +import { + downloadUsageLogsExport, + getUsageLogsExportStatus, + startUsageLogsExport, + type UsageLogsExportStatus, +} from "@/actions/usage-logs"; import { Button } from "@/components/ui/button"; +import { Progress } from "@/components/ui/progress"; +import { getErrorMessage } from "@/lib/utils/error-messages"; import type { Key } from "@/types/key"; import type { ProviderDisplay } from "@/types/provider"; import { ActiveFiltersDisplay } from "./filters/active-filters-display"; @@ -67,10 +74,13 @@ export function UsageLogsFilters({ serverTimeZone, }: UsageLogsFiltersProps) { const t = useTranslations("dashboard"); + const tErrors = useTranslations("errors"); const [localFilters, setLocalFilters] = useState(filters); const [isExporting, setIsExporting] = useState(false); + const [exportStatus, setExportStatus] = useState(null); const [activePreset, setActivePreset] = useState(null); + const exportRunIdRef = useRef(0); // Track users and keys for display name resolution const [availableUsers, setAvailableUsers] = useState>([]); @@ -133,42 +143,133 @@ export function UsageLogsFilters({ return count; }, [localFilters.statusCode, localFilters.excludeStatusCode200, localFilters.minRetryCount]); + useEffect(() => { + setLocalFilters(filters); + }, [filters]); + + useEffect(() => { + return () => { + exportRunIdRef.current += 1; + }; + }, []); + const handleApply = useCallback(() => { onChange(sanitizeFilters(localFilters)); }, [localFilters, onChange]); const handleReset = useCallback(() => { + exportRunIdRef.current += 1; setLocalFilters({}); setKeys([]); setActivePreset(null); + setIsExporting(false); + setExportStatus(null); onReset(); }, [onReset]); + const downloadCsv = useCallback((csv: string) => { + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `usage-logs-${format(new Date(), "yyyy-MM-dd-HHmmss")}.csv`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, []); + const handleExport = async () => { + const runId = exportRunIdRef.current + 1; + exportRunIdRef.current = runId; setIsExporting(true); + setExportStatus({ + jobId: "", + status: "queued", + processedRows: 0, + totalRows: 0, + progressPercent: 0, + }); + try { - const result = await exportUsageLogs(localFilters); - if (!result.ok) { - toast.error(result.error || t("logs.filters.exportError")); + const exportFilters = sanitizeFilters(localFilters); + const startResult = await startUsageLogsExport(exportFilters); + if (exportRunIdRef.current !== runId) { return; } - const blob = new Blob([result.data], { type: "text/csv;charset=utf-8;" }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `usage-logs-${format(new Date(), "yyyy-MM-dd-HHmmss")}.csv`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - window.URL.revokeObjectURL(url); + if (!startResult.ok) { + setExportStatus(null); + console.error("Failed to start usage logs export", startResult.error); + toast.error(t("logs.filters.exportError")); + return; + } + + const jobId = startResult.data.jobId; + const EXPORT_TIMEOUT_MS = 10 * 60 * 1000; + const deadline = Date.now() + EXPORT_TIMEOUT_MS; + + while (true) { + if (exportRunIdRef.current !== runId) { + return; + } + + if (Date.now() > deadline) { + setExportStatus(null); + toast.error(t("logs.filters.exportError")); + return; + } + + const statusResult = await getUsageLogsExportStatus(jobId); + if (exportRunIdRef.current !== runId) { + return; + } + + if (!statusResult.ok) { + setExportStatus(null); + toast.error(t("logs.filters.exportError")); + return; + } + + setExportStatus(statusResult.data); + + if (statusResult.data.status === "failed") { + toast.error(statusResult.data.error || t("logs.filters.exportError")); + return; + } + + if (statusResult.data.status === "completed") { + break; + } + + await new Promise((resolve) => window.setTimeout(resolve, 800)); + } + + const downloadResult = await downloadUsageLogsExport(jobId); + if (exportRunIdRef.current !== runId) { + return; + } + + if (!downloadResult.ok) { + toast.error( + downloadResult.errorCode + ? getErrorMessage(tErrors, downloadResult.errorCode, downloadResult.errorParams) + : t("logs.filters.exportError") + ); + return; + } + + downloadCsv(downloadResult.data); toast.success(t("logs.filters.exportSuccess")); } catch (error) { console.error("Export failed:", error); toast.error(t("logs.filters.exportError")); } finally { - setIsExporting(false); + if (exportRunIdRef.current === runId) { + setExportStatus(null); + setIsExporting(false); + } } }; @@ -326,6 +427,22 @@ export function UsageLogsFilters({
- - - {t("table.time")} - {t("table.model")} - {t("table.tokens")} - {t("table.cacheWrite")} - {t("table.cacheRead")} - {t("table.cost")} - {t("table.status")} - - - - {loading ? ( - Array.from({ length: 6 }).map((_, rowIndex) => ( - - {Array.from({ length: 7 }).map((_, cellIndex) => ( - - - - ))} - - )) - ) : logs.length === 0 ? ( - - - {t("noLogs")} - - - ) : ( - logs.map((log) => ( - - - {log.createdAt - ? formatInTimeZone(new Date(log.createdAt), timeZone, "yyyy-MM-dd HH:mm:ss") - : "-"} - - -
- {log.model ? : null} - {log.model ? ( - handleCopyModel(log.model!)} - > - {log.model} - +
+ {errorMessage ?
{errorMessage}
: null} + +
+ {tDashboard("logs.table.loadedCount", { count: logs.length })} + {errorMessage && onLoadMore ? ( + + {errorMessage} + + + ) : isFetchingNextPage ? ( + + + {tDashboard("logs.table.loadingMore")} + + ) : !hasNextPage ? ( + {tDashboard("logs.table.noMoreData")} + ) : null} +
+ +
+
+
+
+
{t("table.time")}
+
{t("table.model")}
+
{t("table.tokens")}
+
{t("table.cacheWrite")}
+
{t("table.cacheRead")}
+
{t("table.cost")}
+
{t("table.status")}
+
+
+ +
+
+ {virtualItems.map((virtualRow) => { + const isLoaderRow = virtualRow.index >= logs.length; + const log = logs[virtualRow.index]; + + if (isLoaderRow) { + return ( +
+ {errorMessage ? ( + {errorMessage} ) : ( - {t("unknownModel")} + )}
- {log.modelRedirect ? ( -
{log.modelRedirect}
- ) : null} - {log.billingModel && log.billingModel !== log.model ? ( -
- {t("billingModel", { model: log.billingModel })} -
- ) : null} - - -
- {formatTokenAmount(log.inputTokens)} - - {formatTokenAmount(log.outputTokens)} - + ); + } + + return ( +
+
+ {log.createdAt + ? formatInTimeZone(new Date(log.createdAt), timeZone, "yyyy-MM-dd HH:mm:ss") + : "-"}
- - - - - -
- {log.cacheCreationInputTokens && - log.cacheCreationInputTokens > 0 && - log.cacheTtlApplied ? ( - - {log.cacheTtlApplied} - - ) : null} - - {formatTokenAmount(log.cacheCreationInputTokens)} - +
+
+
+ {log.model ? : null} + {log.model ? ( + + ) : ( + {t("unknownModel")} + )} +
+ {log.modelRedirect ? ( +
+ {log.modelRedirect}
- - -
5m: {formatTokenAmount(log.cacheCreation5mInputTokens)}
-
1h: {formatTokenAmount(log.cacheCreation1hInputTokens)}
-
- - - - - {formatTokenAmount(log.cacheReadInputTokens)} - - - {CURRENCY_CONFIG[currencyCode]?.symbol ?? currencyCode} - {Number(log.cost ?? 0).toFixed(4)} - - - = 400 ? "destructive" : "outline"} - className={ - log.statusCode === 200 - ? "border-green-500 text-green-600 dark:text-green-400" - : undefined - } - > - {log.statusCode ?? "-"} - - - - )) - )} - -
-
- -
- - {loading && loadingLabel - ? loadingLabel - : t("pagination", { - from: (page - 1) * pageSize + 1, - to: Math.min(page * pageSize, total), - total, + ) : null} + {log.billingModel && log.billingModel !== log.model ? ( +
+ {t("billingModel", { model: log.billingModel })} +
+ ) : null} +
+ +
+
+ {formatTokenAmount(log.inputTokens)} + + {formatTokenAmount(log.outputTokens)} + +
+
+
+ + + +
+ {log.cacheCreationInputTokens && + log.cacheCreationInputTokens > 0 && + log.cacheTtlApplied ? ( + + {log.cacheTtlApplied} + + ) : null} + + {formatTokenAmount(log.cacheCreationInputTokens)} + +
+
+ +
5m: {formatTokenAmount(log.cacheCreation5mInputTokens)}
+
1h: {formatTokenAmount(log.cacheCreation1hInputTokens)}
+
+
+
+
+
+ {formatTokenAmount(log.cacheReadInputTokens)} +
+
+ {CURRENCY_CONFIG[currencyCode]?.symbol ?? currencyCode} + {Number(log.cost ?? 0).toFixed(4)} +
+
+ = 400 ? "destructive" : "outline" + } + className={ + log.statusCode === 200 + ? "border-green-500 text-green-600 dark:text-green-400" + : undefined + } + > + {log.statusCode ?? "-"} + +
+ + ); })} - -
- - - {page}/{totalPages} - - +
+ + + {showScrollToTop ? ( + + ) : null} ); } diff --git a/src/app/[locale]/my-usage/page.tsx b/src/app/[locale]/my-usage/page.tsx index 265b79628..c88f0f729 100644 --- a/src/app/[locale]/my-usage/page.tsx +++ b/src/app/[locale]/my-usage/page.tsx @@ -1,12 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import { - getMyQuota, - getMyUsageLogs, - type MyUsageLogsResult, - type MyUsageQuota, -} from "@/actions/my-usage"; +import { getMyQuota, type MyUsageQuota } from "@/actions/my-usage"; import { getServerTimeZone } from "@/actions/system-config"; import { useRouter } from "@/i18n/routing"; import { CollapsibleQuotaCard } from "./_components/collapsible-quota-card"; @@ -20,14 +15,11 @@ export default function MyUsagePage() { const router = useRouter(); const [quota, setQuota] = useState(null); - const [logsData, setLogsData] = useState(null); const [isQuotaLoading, setIsQuotaLoading] = useState(true); - const [isLogsLoading, setIsLogsLoading] = useState(true); const [serverTimeZone, setServerTimeZone] = useState(undefined); const loadInitial = useCallback(() => { setIsQuotaLoading(true); - setIsLogsLoading(true); void getMyQuota() .then((quotaResult) => { @@ -35,12 +27,6 @@ export default function MyUsagePage() { }) .finally(() => setIsQuotaLoading(false)); - void getMyUsageLogs({ page: 1 }) - .then((logsResult) => { - if (logsResult.ok) setLogsData(logsResult.data ?? null); - }) - .finally(() => setIsLogsLoading(false)); - void getServerTimeZone().then((tzResult) => { if (tzResult.ok) setServerTimeZone(tzResult.data.timeZone); }); @@ -85,12 +71,7 @@ export default function MyUsagePage() { - + ); } diff --git a/src/app/[locale]/settings/config/_components/system-settings-form.tsx b/src/app/[locale]/settings/config/_components/system-settings-form.tsx index 5fdcb4057..21442d6d6 100644 --- a/src/app/[locale]/settings/config/_components/system-settings-form.tsx +++ b/src/app/[locale]/settings/config/_components/system-settings-form.tsx @@ -41,7 +41,11 @@ import { shouldWarnQuotaLeaseCapZero, shouldWarnQuotaLeasePercentZero, } from "@/lib/utils/validation/quota-lease-warnings"; -import type { BillingModelSource, SystemSettings } from "@/types/system-config"; +import type { + BillingModelSource, + CodexPriorityBillingSource, + SystemSettings, +} from "@/types/system-config"; interface SystemSettingsFormProps { initialSettings: Pick< @@ -50,6 +54,7 @@ interface SystemSettingsFormProps { | "allowGlobalUsageView" | "currencyDisplay" | "billingModelSource" + | "codexPriorityBillingSource" | "timezone" | "verboseProviderError" | "enableHttp2" @@ -92,6 +97,8 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) const [billingModelSource, setBillingModelSource] = useState( initialSettings.billingModelSource ); + const [codexPriorityBillingSource, setCodexPriorityBillingSource] = + useState(initialSettings.codexPriorityBillingSource); const [timezone, setTimezone] = useState(initialSettings.timezone); const [verboseProviderError, setVerboseProviderError] = useState( initialSettings.verboseProviderError @@ -170,6 +177,7 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) allowGlobalUsageView, currencyDisplay, billingModelSource, + codexPriorityBillingSource, timezone, verboseProviderError, enableHttp2, @@ -200,6 +208,7 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) setAllowGlobalUsageView(result.data.allowGlobalUsageView); setCurrencyDisplay(result.data.currencyDisplay); setBillingModelSource(result.data.billingModelSource); + setCodexPriorityBillingSource(result.data.codexPriorityBillingSource); setTimezone(result.data.timezone); setVerboseProviderError(result.data.verboseProviderError); setEnableHttp2(result.data.enableHttp2); @@ -301,6 +310,33 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps)

{t("billingModelSourceDesc")}

+
+ + +

{t("codexPriorityBillingSourceDesc")}

+
+ {/* Timezone Select */}