Переглянути джерело

feat: add provider scheduled active time & remove joinClaudePool (#844)

* feat: add provider scheduled active time and remove deprecated joinClaudePool

Add HH:mm-based active time windows (start/end) to providers for
time-based routing control. Supports same-day and cross-day schedules
with system timezone integration.

Also removes the deprecated joinClaudePool column/feature.

Key changes:
- Core: isProviderActiveNow() with NaN fail-open defense
- Schema: active_time_start/end columns, drop join_claude_pool (single migration)
- Types: 7 interfaces updated across Provider/Batch/Create/Update
- Proxy: schedule_inactive filter in provider selection hot path
- Batch: HH:mm regex validation in patch contract
- UI: schedule toggle, time pickers, cross-day hint, list badge
- i18n: 5 languages (zh-CN, zh-TW, en, ja, ru)
- Tests: 25 schedule tests + 10 batch patch tests

* fix: address code review findings from PR #844

1. Add schedule check in findReusable session reuse path to prevent
   bound sessions from bypassing active time window filtering.
   Clears session binding when provider is outside schedule.

2. Strict HH:mm validation in parseHHMM using regex - values like
   "24:00", "9:00", "99:99" now return NaN and trigger fail-open,
   instead of producing silently incorrect minute values.
Ding 1 місяць тому
батько
коміт
4ad4ffa6f6
36 змінених файлів з 4683 додано та 20 видалено
  1. 3 0
      drizzle/0077_nappy_giant_man.sql
  2. 3908 0
      drizzle/meta/0077_snapshot.json
  3. 7 0
      drizzle/meta/_journal.json
  4. 3 1
      messages/en/settings/providers/batchEdit.json
  5. 10 0
      messages/en/settings/providers/form/sections.json
  6. 2 1
      messages/en/settings/providers/list.json
  7. 3 1
      messages/ja/settings/providers/batchEdit.json
  8. 10 0
      messages/ja/settings/providers/form/sections.json
  9. 2 1
      messages/ja/settings/providers/list.json
  10. 3 1
      messages/ru/settings/providers/batchEdit.json
  11. 10 0
      messages/ru/settings/providers/form/sections.json
  12. 2 1
      messages/ru/settings/providers/list.json
  13. 3 1
      messages/zh-CN/settings/providers/batchEdit.json
  14. 10 0
      messages/zh-CN/settings/providers/form/sections.json
  15. 2 1
      messages/zh-CN/settings/providers/list.json
  16. 3 1
      messages/zh-TW/settings/providers/batchEdit.json
  17. 10 0
      messages/zh-TW/settings/providers/form/sections.json
  18. 2 1
      messages/zh-TW/settings/providers/list.json
  19. 16 0
      src/actions/providers.ts
  20. 14 0
      src/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts
  21. 2 0
      src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
  22. 16 0
      src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
  23. 5 0
      src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
  24. 69 1
      src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
  25. 15 0
      src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
  26. 34 2
      src/app/v1/_lib/proxy/provider-selector.ts
  27. 4 3
      src/drizzle/schema.ts
  28. 32 0
      src/lib/provider-patch-contract.ts
  29. 79 0
      src/lib/utils/provider-schedule.ts
  30. 55 0
      src/lib/validation/schemas.ts
  31. 2 0
      src/repository/_shared/transformers.ts
  32. 24 0
      src/repository/provider.ts
  33. 1 0
      src/types/message.ts
  34. 23 4
      src/types/provider.ts
  35. 121 0
      tests/unit/actions/providers-patch-contract.test.ts
  36. 178 0
      tests/unit/lib/utils/provider-schedule.test.ts

+ 3 - 0
drizzle/0077_nappy_giant_man.sql

@@ -0,0 +1,3 @@
+ALTER TABLE "providers" ADD COLUMN "active_time_start" varchar(5);--> statement-breakpoint
+ALTER TABLE "providers" ADD COLUMN "active_time_end" varchar(5);--> statement-breakpoint
+ALTER TABLE "providers" DROP COLUMN "join_claude_pool";

+ 3908 - 0
drizzle/meta/0077_snapshot.json

@@ -0,0 +1,3908 @@
+{
+  "id": "22eb3652-56d7-4a04-9845-5fa18210ef90",
+  "prevId": "ea98464d-5d4d-45e1-b04f-9d5b5b280601",
+  "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
+        },
+        "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(50)",
+          "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
+        },
+        "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
+        },
+        "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": {}
+        }
+      },
+      "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'"
+        },
+        "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_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": {}
+        }
+      },
+      "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
+        },
+        "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": {}
+  }
+}

+ 7 - 0
drizzle/meta/_journal.json

@@ -540,6 +540,13 @@
       "when": 1771954926766,
       "tag": "0076_mighty_lionheart",
       "breakpoints": true
+    },
+    {
+      "idx": 77,
+      "version": "7",
+      "when": 1772219877045,
+      "tag": "0077_nappy_giant_man",
+      "breakpoints": true
     }
   ]
 }

+ 3 - 1
messages/en/settings/providers/batchEdit.json

@@ -46,7 +46,9 @@
     "modelRedirects": "Model Redirects",
     "allowedModels": "Allowed Models",
     "thinkingBudget": "Thinking Budget",
-    "adaptiveThinking": "Adaptive Thinking"
+    "adaptiveThinking": "Adaptive Thinking",
+    "activeTimeStart": "Active Start Time",
+    "activeTimeEnd": "Active End Time"
   },
   "affectedProviders": {
     "title": "Affected Providers",

+ 10 - 0
messages/en/settings/providers/form/sections.json

@@ -368,6 +368,16 @@
         "placeholder": "1"
       }
     },
+    "activeTime": {
+      "title": "Scheduled Active Time",
+      "description": "Automatically activate provider during specific time windows",
+      "toggleLabel": "Enable Schedule",
+      "toggleDescription": "When enabled, this provider is only active during the specified time window",
+      "startLabel": "Start Time",
+      "endLabel": "End Time",
+      "timezoneNote": "Times are based on system timezone",
+      "crossDayHint": "Cross-day schedule: active from {start} to next day {end}"
+    },
     "summary": {
       "models": "{count} whitelisted models",
       "none": "Not configured",

+ 2 - 1
messages/en/settings/providers/list.json

@@ -41,5 +41,6 @@
   "actionResetCircuit": "Reset Circuit",
   "actionResetUsage": "Reset Usage",
   "actionDelete": "Delete",
-  "selectProvider": "Select {name}"
+  "selectProvider": "Select {name}",
+  "schedule": "Schedule"
 }

+ 3 - 1
messages/ja/settings/providers/batchEdit.json

@@ -46,7 +46,9 @@
     "modelRedirects": "モデルリダイレクト",
     "allowedModels": "許可モデル",
     "thinkingBudget": "思考バジェット",
-    "adaptiveThinking": "アダプティブ思考"
+    "adaptiveThinking": "アダプティブ思考",
+    "activeTimeStart": "スケジュール開始時刻",
+    "activeTimeEnd": "スケジュール終了時刻"
   },
   "affectedProviders": {
     "title": "影響を受けるプロバイダー",

+ 10 - 0
messages/ja/settings/providers/form/sections.json

@@ -369,6 +369,16 @@
         "placeholder": "1"
       }
     },
+    "activeTime": {
+      "title": "スケジュール有効時間",
+      "description": "指定した時間帯に自動的にプロバイダーを有効化します",
+      "toggleLabel": "スケジュールを有効化",
+      "toggleDescription": "有効にすると、指定した時間帯のみプロバイダーがアクティブになります",
+      "startLabel": "開始時刻",
+      "endLabel": "終了時刻",
+      "timezoneNote": "時刻はシステムのタイムゾーンに基づきます",
+      "crossDayHint": "日跨ぎスケジュール: {start} から翌日 {end} まで有効"
+    },
     "summary": {
       "models": "許可モデル {count} 件",
       "none": "未設定",

+ 2 - 1
messages/ja/settings/providers/list.json

@@ -41,5 +41,6 @@
   "actionResetCircuit": "サーキットリセット",
   "actionResetUsage": "使用量リセット",
   "actionDelete": "削除",
-  "selectProvider": "{name} を選択"
+  "selectProvider": "{name} を選択",
+  "schedule": "スケジュール"
 }

+ 3 - 1
messages/ru/settings/providers/batchEdit.json

@@ -46,7 +46,9 @@
     "modelRedirects": "Перенаправление моделей",
     "allowedModels": "Разрешённые модели",
     "thinkingBudget": "Бюджет мышления",
-    "adaptiveThinking": "Адаптивное мышление"
+    "adaptiveThinking": "Адаптивное мышление",
+    "activeTimeStart": "Время начала расписания",
+    "activeTimeEnd": "Время окончания расписания"
   },
   "affectedProviders": {
     "title": "Затронутые поставщики",

+ 10 - 0
messages/ru/settings/providers/form/sections.json

@@ -369,6 +369,16 @@
         "placeholder": "1"
       }
     },
+    "activeTime": {
+      "title": "Расписание активности",
+      "description": "Автоматически активировать провайдера в указанные временные окна",
+      "toggleLabel": "Включить расписание",
+      "toggleDescription": "При включении провайдер активен только в указанном временном окне",
+      "startLabel": "Время начала",
+      "endLabel": "Время окончания",
+      "timezoneNote": "Время указано по системному часовому поясу",
+      "crossDayHint": "Расписание через полночь: активен с {start} до {end} следующего дня"
+    },
     "summary": {
       "models": "{count} моделей в белом списке",
       "none": "Не настроено",

+ 2 - 1
messages/ru/settings/providers/list.json

@@ -41,5 +41,6 @@
   "actionResetCircuit": "Сбросить автоматический выключатель",
   "actionResetUsage": "Сбросить использование",
   "actionDelete": "Удалить",
-  "selectProvider": "Выбрать {name}"
+  "selectProvider": "Выбрать {name}",
+  "schedule": "Расписание"
 }

+ 3 - 1
messages/zh-CN/settings/providers/batchEdit.json

@@ -46,7 +46,9 @@
     "modelRedirects": "模型重定向",
     "allowedModels": "允许的模型",
     "thinkingBudget": "思维预算",
-    "adaptiveThinking": "自适应思维"
+    "adaptiveThinking": "自适应思维",
+    "activeTimeStart": "调度开始时间",
+    "activeTimeEnd": "调度结束时间"
   },
   "affectedProviders": {
     "title": "受影响的供应商",

+ 10 - 0
messages/zh-CN/settings/providers/form/sections.json

@@ -100,6 +100,16 @@
         "noGroups": "请先设置分组标签,才能配置分组优先级"
       }
     },
+    "activeTime": {
+      "title": "调度时间窗口",
+      "description": "在指定时间窗口内自动激活供应商",
+      "toggleLabel": "启用时间调度",
+      "toggleDescription": "启用后,此供应商仅在指定时间窗口内处于活跃状态",
+      "startLabel": "开始时间",
+      "endLabel": "结束时间",
+      "timezoneNote": "时间基于系统时区",
+      "crossDayHint": "跨天调度: 从 {start} 到次日 {end}"
+    },
     "cacheTtl": {
       "label": "Cache TTL 覆写",
       "options": {

+ 2 - 1
messages/zh-CN/settings/providers/list.json

@@ -41,5 +41,6 @@
   "actionResetCircuit": "重置熔断",
   "actionResetUsage": "重置用量",
   "actionDelete": "删除",
-  "selectProvider": "选择 {name}"
+  "selectProvider": "选择 {name}",
+  "schedule": "调度"
 }

+ 3 - 1
messages/zh-TW/settings/providers/batchEdit.json

@@ -46,7 +46,9 @@
     "modelRedirects": "模型重新導向",
     "allowedModels": "允許的模型",
     "thinkingBudget": "思維預算",
-    "adaptiveThinking": "自適應思維"
+    "adaptiveThinking": "自適應思維",
+    "activeTimeStart": "排程開始時間",
+    "activeTimeEnd": "排程結束時間"
   },
   "affectedProviders": {
     "title": "受影響的供應商",

+ 10 - 0
messages/zh-TW/settings/providers/form/sections.json

@@ -369,6 +369,16 @@
         "placeholder": "1"
       }
     },
+    "activeTime": {
+      "title": "排程時間窗口",
+      "description": "在指定時間窗口內自動啟用供應商",
+      "toggleLabel": "啟用時間排程",
+      "toggleDescription": "啟用後,此供應商僅在指定時間窗口內處於活躍狀態",
+      "startLabel": "開始時間",
+      "endLabel": "結束時間",
+      "timezoneNote": "時間依據系統時區",
+      "crossDayHint": "跨天排程: 從 {start} 到隔日 {end}"
+    },
     "summary": {
       "models": "{count} 個允許模型",
       "none": "未設定",

+ 2 - 1
messages/zh-TW/settings/providers/list.json

@@ -41,5 +41,6 @@
   "actionResetCircuit": "重置熔斷",
   "actionResetUsage": "重設用量",
   "actionDelete": "刪除",
-  "selectProvider": "選擇 {name}"
+  "selectProvider": "選擇 {name}",
+  "schedule": "排程"
 }

+ 16 - 0
src/actions/providers.ts

@@ -274,6 +274,8 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
         providerVendorId: provider.providerVendorId,
         preserveClientIp: provider.preserveClientIp,
         modelRedirects: provider.modelRedirects,
+        activeTimeStart: provider.activeTimeStart,
+        activeTimeEnd: provider.activeTimeEnd,
         allowedModels: provider.allowedModels,
         allowedClients: provider.allowedClients,
         blockedClients: provider.blockedClients,
@@ -480,6 +482,8 @@ export async function addProvider(data: {
   provider_type?: ProviderType;
   preserve_client_ip?: boolean;
   model_redirects?: Record<string, string> | null;
+  active_time_start?: string | null;
+  active_time_end?: string | null;
   allowed_models?: string[] | null;
   allowed_clients?: string[] | null;
   blocked_clients?: string[] | null;
@@ -653,6 +657,8 @@ export async function editProvider(
     provider_type?: ProviderType;
     preserve_client_ip?: boolean;
     model_redirects?: Record<string, string> | null;
+    active_time_start?: string | null;
+    active_time_end?: string | null;
     allowed_models?: string[] | null;
     allowed_clients?: string[] | null;
     blocked_clients?: string[] | null;
@@ -1266,6 +1272,8 @@ const SINGLE_EDIT_PREIMAGE_FIELD_TO_PROVIDER_KEY: Record<string, keyof Provider>
   group_priorities: "groupPriorities",
   provider_type: "providerType",
   preserve_client_ip: "preserveClientIp",
+  active_time_start: "activeTimeStart",
+  active_time_end: "activeTimeEnd",
   model_redirects: "modelRedirects",
   allowed_models: "allowedModels",
   limit_5h_usd: "limit5hUsd",
@@ -1427,6 +1435,12 @@ function mapApplyUpdatesToRepositoryFormat(
   if (applyUpdates.preserve_client_ip !== undefined) {
     result.preserveClientIp = applyUpdates.preserve_client_ip;
   }
+  if (applyUpdates.active_time_start !== undefined) {
+    result.activeTimeStart = applyUpdates.active_time_start;
+  }
+  if (applyUpdates.active_time_end !== undefined) {
+    result.activeTimeEnd = applyUpdates.active_time_end;
+  }
   if (applyUpdates.group_priorities !== undefined) {
     result.groupPriorities = applyUpdates.group_priorities;
   }
@@ -1536,6 +1550,8 @@ const PATCH_FIELD_TO_PROVIDER_KEY: Record<ProviderBatchPatchField, keyof Provide
   anthropic_thinking_budget_preference: "anthropicThinkingBudgetPreference",
   anthropic_adaptive_thinking: "anthropicAdaptiveThinking",
   preserve_client_ip: "preserveClientIp",
+  active_time_start: "activeTimeStart",
+  active_time_end: "activeTimeEnd",
   group_priorities: "groupPriorities",
   cache_ttl_preference: "cacheTtlPreference",
   swap_cache_ttl_billing: "swapCacheTtlBilling",

+ 14 - 0
src/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts

@@ -43,6 +43,20 @@ export function buildPatchDraftFromFormState(
   if (dirtyFields.has("routing.preserveClientIp")) {
     draft.preserve_client_ip = { set: state.routing.preserveClientIp };
   }
+  if (dirtyFields.has("routing.activeTimeStart")) {
+    if (state.routing.activeTimeStart === null) {
+      draft.active_time_start = { clear: true };
+    } else {
+      draft.active_time_start = { set: state.routing.activeTimeStart };
+    }
+  }
+  if (dirtyFields.has("routing.activeTimeEnd")) {
+    if (state.routing.activeTimeEnd === null) {
+      draft.active_time_end = { clear: true };
+    } else {
+      draft.active_time_end = { set: state.routing.activeTimeEnd };
+    }
+  }
   if (dirtyFields.has("routing.modelRedirects")) {
     const entries = Object.keys(state.routing.modelRedirects);
     if (entries.length === 0) {

+ 2 - 0
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx

@@ -339,6 +339,8 @@ function ProviderFormContent({
           anthropic_thinking_budget_preference: state.routing.anthropicThinkingBudgetPreference,
           anthropic_adaptive_thinking: state.routing.anthropicAdaptiveThinking,
           gemini_google_search_preference: state.routing.geminiGoogleSearchPreference,
+          active_time_start: state.routing.activeTimeStart || null,
+          active_time_end: state.routing.activeTimeEnd || null,
           limit_5h_usd: state.rateLimit.limit5hUsd,
           limit_daily_usd: state.rateLimit.limitDailyUsd,
           daily_reset_mode: state.rateLimit.dailyResetMode,

+ 16 - 0
src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx

@@ -45,6 +45,8 @@ const ACTION_TO_FIELD_PATH: Partial<Record<ProviderFormAction["type"], string>>
   SET_ADAPTIVE_THINKING_MODEL_MATCH_MODE: "routing.anthropicAdaptiveThinking",
   SET_ADAPTIVE_THINKING_MODELS: "routing.anthropicAdaptiveThinking",
   SET_GEMINI_GOOGLE_SEARCH: "routing.geminiGoogleSearchPreference",
+  SET_ACTIVE_TIME_START: "routing.activeTimeStart",
+  SET_ACTIVE_TIME_END: "routing.activeTimeEnd",
   SET_LIMIT_5H_USD: "rateLimit.limit5hUsd",
   SET_LIMIT_DAILY_USD: "rateLimit.limitDailyUsd",
   SET_DAILY_RESET_MODE: "rateLimit.dailyResetMode",
@@ -110,6 +112,8 @@ export function createInitialState(
         anthropicThinkingBudgetPreference: "inherit",
         anthropicAdaptiveThinking: null,
         geminiGoogleSearchPreference: "inherit",
+        activeTimeStart: null,
+        activeTimeEnd: null,
       },
       rateLimit: {
         limit5hUsd: null,
@@ -190,6 +194,8 @@ export function createInitialState(
         sourceProvider?.anthropicThinkingBudgetPreference ?? "inherit",
       anthropicAdaptiveThinking: sourceProvider?.anthropicAdaptiveThinking ?? null,
       geminiGoogleSearchPreference: sourceProvider?.geminiGoogleSearchPreference ?? "inherit",
+      activeTimeStart: sourceProvider?.activeTimeStart ?? null,
+      activeTimeEnd: sourceProvider?.activeTimeEnd ?? null,
     },
     rateLimit: {
       limit5hUsd: sourceProvider?.limit5hUsd ?? null,
@@ -375,6 +381,16 @@ export function providerFormReducer(
         ...state,
         routing: { ...state.routing, geminiGoogleSearchPreference: action.payload },
       };
+    case "SET_ACTIVE_TIME_START":
+      return {
+        ...state,
+        routing: { ...state.routing, activeTimeStart: action.payload },
+      };
+    case "SET_ACTIVE_TIME_END":
+      return {
+        ...state,
+        routing: { ...state.routing, activeTimeEnd: action.payload },
+      };
 
     // Rate limit
     case "SET_LIMIT_5H_USD":

+ 5 - 0
src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts

@@ -62,6 +62,9 @@ export interface RoutingState {
   anthropicAdaptiveThinking: AnthropicAdaptiveThinkingConfig | null;
   // Gemini-specific
   geminiGoogleSearchPreference: GeminiGoogleSearchPreference;
+  // Scheduled active time window (HH:mm format, null = always active)
+  activeTimeStart: string | null;
+  activeTimeEnd: string | null;
 }
 
 export interface RateLimitState {
@@ -153,6 +156,8 @@ export type ProviderFormAction =
   | { type: "SET_ADAPTIVE_THINKING_MODELS"; payload: string[] }
   | { type: "SET_ADAPTIVE_THINKING_ENABLED"; payload: boolean }
   | { type: "SET_GEMINI_GOOGLE_SEARCH"; payload: GeminiGoogleSearchPreference }
+  | { type: "SET_ACTIVE_TIME_START"; payload: string | null }
+  | { type: "SET_ACTIVE_TIME_END"; payload: string | null }
   // Rate limit actions
   | { type: "SET_LIMIT_5H_USD"; payload: number | null }
   | { type: "SET_LIMIT_DAILY_USD"; payload: number | null }

+ 69 - 1
src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx

@@ -1,7 +1,7 @@
 "use client";
 
 import { motion } from "framer-motion";
-import { Info, Layers, Route, Scale, Settings, Timer } from "lucide-react";
+import { Clock, Info, Layers, Route, Scale, Settings, Timer } from "lucide-react";
 import { useTranslations } from "next-intl";
 import { useEffect, useState } from "react";
 import { toast } from "sonner";
@@ -817,6 +817,74 @@ export function RoutingSection() {
             </SmartInputWrapper>
           </SectionCard>
         )}
+
+        {/* Scheduled Active Time */}
+        <SectionCard
+          title={t("sections.routing.activeTime.title")}
+          description={t("sections.routing.activeTime.description")}
+          icon={Clock}
+        >
+          <div className="space-y-4">
+            <ToggleRow
+              label={t("sections.routing.activeTime.toggleLabel")}
+              description={t("sections.routing.activeTime.toggleDescription")}
+            >
+              <Switch
+                checked={
+                  state.routing.activeTimeStart !== null && state.routing.activeTimeEnd !== null
+                }
+                onCheckedChange={(checked) => {
+                  if (checked) {
+                    dispatch({ type: "SET_ACTIVE_TIME_START", payload: "09:00" });
+                    dispatch({ type: "SET_ACTIVE_TIME_END", payload: "22:00" });
+                  } else {
+                    dispatch({ type: "SET_ACTIVE_TIME_START", payload: null });
+                    dispatch({ type: "SET_ACTIVE_TIME_END", payload: null });
+                  }
+                }}
+                disabled={state.ui.isPending}
+              />
+            </ToggleRow>
+
+            {state.routing.activeTimeStart !== null && state.routing.activeTimeEnd !== null && (
+              <div className="space-y-3">
+                <div className="grid grid-cols-2 gap-4">
+                  <SmartInputWrapper label={t("sections.routing.activeTime.startLabel")}>
+                    <Input
+                      type="time"
+                      value={state.routing.activeTimeStart}
+                      onChange={(e) =>
+                        dispatch({ type: "SET_ACTIVE_TIME_START", payload: e.target.value })
+                      }
+                      disabled={state.ui.isPending}
+                    />
+                  </SmartInputWrapper>
+                  <SmartInputWrapper label={t("sections.routing.activeTime.endLabel")}>
+                    <Input
+                      type="time"
+                      value={state.routing.activeTimeEnd}
+                      onChange={(e) =>
+                        dispatch({ type: "SET_ACTIVE_TIME_END", payload: e.target.value })
+                      }
+                      disabled={state.ui.isPending}
+                    />
+                  </SmartInputWrapper>
+                </div>
+                <p className="text-xs text-muted-foreground">
+                  {t("sections.routing.activeTime.timezoneNote")}
+                </p>
+                {state.routing.activeTimeStart > state.routing.activeTimeEnd && (
+                  <p className="text-xs text-amber-600">
+                    {t("sections.routing.activeTime.crossDayHint", {
+                      start: state.routing.activeTimeStart,
+                      end: state.routing.activeTimeEnd,
+                    })}
+                  </p>
+                )}
+              </div>
+            )}
+          </div>
+        </SectionCard>
       </motion.div>
     </TooltipProvider>
   );

+ 15 - 0
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx

@@ -4,6 +4,7 @@ import { useQueryClient } from "@tanstack/react-query";
 import {
   AlertTriangle,
   CheckCircle,
+  Clock,
   Copy,
   Edit,
   Globe,
@@ -539,6 +540,13 @@ export function ProviderRichListItem({
               {tList("endpointCircuitBroken")}
             </Badge>
           )}
+          {/* Schedule badge */}
+          {provider.activeTimeStart && provider.activeTimeEnd && (
+            <Badge variant="outline" className="flex items-center gap-1">
+              <Clock className="h-3 w-3" />
+              {provider.activeTimeStart}-{provider.activeTimeEnd}
+            </Badge>
+          )}
         </div>
 
         {/* Mobile: metrics row */}
@@ -738,6 +746,13 @@ export function ProviderRichListItem({
                 {tList("endpointCircuitBroken")}
               </Badge>
             )}
+            {/* Schedule badge */}
+            {provider.activeTimeStart && provider.activeTimeEnd && (
+              <Badge variant="outline" className="flex items-center gap-1 flex-shrink-0">
+                <Clock className="h-3 w-3" />
+                {provider.activeTimeStart}-{provider.activeTimeEnd}
+              </Badge>
+            )}
           </div>
           <div className="flex items-center gap-3 mt-1 text-sm text-muted-foreground flex-wrap">
             {/* Vendor & Endpoints OR Legacy URL */}

+ 34 - 2
src/app/v1/_lib/proxy/provider-selector.ts

@@ -3,6 +3,8 @@ import { PROVIDER_GROUP } from "@/lib/constants/provider.constants";
 import { logger } from "@/lib/logger";
 import { RateLimitService } from "@/lib/rate-limit";
 import { SessionManager } from "@/lib/session-manager";
+import { isProviderActiveNow } from "@/lib/utils/provider-schedule";
+import { resolveSystemTimezone } from "@/lib/utils/timezone";
 import { isVendorTypeCircuitOpen } from "@/lib/vendor-type-circuit-breaker";
 import { findAllProviders, findProviderById } from "@/repository/provider";
 import { getSystemSettings } from "@/repository/system-config";
@@ -536,6 +538,20 @@ export class ProxyProviderResolver {
       return null;
     }
 
+    // 调度时间窗口检查:防止会话复用绕过时间调度
+    const systemTimezone = await resolveSystemTimezone();
+    if (!isProviderActiveNow(provider.activeTimeStart, provider.activeTimeEnd, systemTimezone)) {
+      logger.debug("ProviderSelector: Session provider outside active schedule", {
+        sessionId: session.sessionId,
+        providerId: provider.id,
+        activeTimeStart: provider.activeTimeStart,
+        activeTimeEnd: provider.activeTimeEnd,
+        timezone: systemTimezone,
+      });
+      await SessionManager.clearSessionProvider(session.sessionId);
+      return null;
+    }
+
     // 临时熔断(vendor+type):防止会话复用绕过故障隔离
     if (
       provider.providerVendorId &&
@@ -844,6 +860,9 @@ export class ProxyProviderResolver {
       visibleProviders = clientFilteredProviders;
     }
 
+    // Resolve system timezone once for active time checks
+    const systemTimezone = await resolveSystemTimezone();
+
     // Step 2: 基础过滤 + 格式/模型匹配(使用 visibleProviders)
     const enabledProviders = visibleProviders.filter((provider) => {
       // 2a. 基础过滤
@@ -851,6 +870,11 @@ export class ProxyProviderResolver {
         return false;
       }
 
+      // 2a-2. 调度时间窗口过滤
+      if (!isProviderActiveNow(provider.activeTimeStart, provider.activeTimeEnd, systemTimezone)) {
+        return false;
+      }
+
       // 2b. 格式类型匹配(新增)
       // 根据 session.originalFormat 限制候选供应商类型,避免格式错配
       if (session?.originalFormat) {
@@ -885,6 +909,7 @@ export class ProxyProviderResolver {
           | "type_mismatch"
           | "model_not_allowed"
           | "context_1m_disabled"
+          | "schedule_inactive"
           | "disabled" = "disabled";
         let details = "";
 
@@ -894,6 +919,9 @@ export class ProxyProviderResolver {
         } else if (excludeIds.includes(p.id)) {
           reason = "excluded";
           details = "已在前序尝试中失败";
+        } else if (!isProviderActiveNow(p.activeTimeStart, p.activeTimeEnd, systemTimezone)) {
+          reason = "schedule_inactive";
+          details = `outside active window ${p.activeTimeStart}-${p.activeTimeEnd}`;
         } else if (
           session?.originalFormat &&
           !checkFormatProviderTypeCompatibility(session.originalFormat, p.providerType)
@@ -1252,9 +1280,13 @@ export class ProxyProviderResolver {
       );
     }
 
-    // 按 providerType 精确过滤
+    // 按 providerType 精确过滤 + 调度时间窗口
+    const systemTimezone = await resolveSystemTimezone();
     const typeFiltered = visibleProviders.filter(
-      (p) => p.isEnabled && p.providerType === providerType
+      (p) =>
+        p.isEnabled &&
+        p.providerType === providerType &&
+        isProviderActiveNow(p.activeTimeStart, p.activeTimeEnd, systemTimezone)
     );
 
     // 将 providerType 映射为 decisionContext 允许的 targetType

+ 4 - 3
src/drizzle/schema.ts

@@ -204,9 +204,10 @@ export const providers = pgTable('providers', {
   allowedClients: jsonb('allowed_clients').$type<string[]>().notNull().default([]),
   blockedClients: jsonb('blocked_clients').$type<string[]>().notNull().default([]),
 
-  // 加入 Claude 调度池:仅对非 Anthropic 提供商有效
-  // 启用后,如果该提供商配置了重定向到 claude-* 模型,可以加入 claude 调度池
-  joinClaudePool: boolean('join_claude_pool').default(false),
+  // Scheduled active time window (HH:mm format)
+  // Both null = always active; both set = active during window only
+  activeTimeStart: varchar('active_time_start', { length: 5 }),
+  activeTimeEnd: varchar('active_time_end', { length: 5 }),
 
   // Codex instructions 策略(已废弃):历史字段保留以兼容旧数据
   // 当前运行时对 Codex 请求的 instructions 一律透传,不再读取/生效此配置

+ 32 - 0
src/lib/provider-patch-contract.ts

@@ -36,6 +36,8 @@ const PATCH_FIELDS: ProviderBatchPatchField[] = [
   "anthropic_thinking_budget_preference",
   "anthropic_adaptive_thinking",
   // Routing
+  "active_time_start",
+  "active_time_end",
   "preserve_client_ip",
   "group_priorities",
   "cache_ttl_preference",
@@ -86,6 +88,8 @@ const CLEARABLE_FIELDS: Record<ProviderBatchPatchField, boolean> = {
   anthropic_thinking_budget_preference: true,
   anthropic_adaptive_thinking: true,
   // Routing
+  active_time_start: true,
+  active_time_end: true,
   preserve_client_ip: false,
   group_priorities: true,
   cache_ttl_preference: true,
@@ -232,6 +236,9 @@ function isValidSetValue(field: ProviderBatchPatchField, value: unknown): boolea
     case "proxy_url":
     case "mcp_passthrough_url":
       return typeof value === "string";
+    case "active_time_start":
+    case "active_time_end":
+      return typeof value === "string" && /^([01][0-9]|2[0-3]):[0-5][0-9]$/.test(value);
     case "group_priorities":
       return isNumberRecord(value);
     case "cache_ttl_preference":
@@ -421,6 +428,12 @@ export function normalizeProviderBatchPatchDraft(
   if (!adaptiveThinking.ok) return adaptiveThinking;
 
   // Routing
+  const activeTimeStart = normalizePatchField("active_time_start", typedDraft.active_time_start);
+  if (!activeTimeStart.ok) return activeTimeStart;
+
+  const activeTimeEnd = normalizePatchField("active_time_end", typedDraft.active_time_end);
+  if (!activeTimeEnd.ok) return activeTimeEnd;
+
   const preserveClientIp = normalizePatchField("preserve_client_ip", typedDraft.preserve_client_ip);
   if (!preserveClientIp.ok) return preserveClientIp;
 
@@ -584,6 +597,8 @@ export function normalizeProviderBatchPatchDraft(
       anthropic_thinking_budget_preference: thinkingBudget.data,
       anthropic_adaptive_thinking: adaptiveThinking.data,
       // Routing
+      active_time_start: activeTimeStart.data,
+      active_time_end: activeTimeEnd.data,
       preserve_client_ip: preserveClientIp.data,
       group_priorities: groupPriorities.data,
       cache_ttl_preference: cacheTtlPref.data,
@@ -672,6 +687,12 @@ function applyPatchField<T>(
           patch.value as ProviderBatchApplyUpdates["anthropic_adaptive_thinking"];
         return { ok: true, data: undefined };
       // Routing
+      case "active_time_start":
+        updates.active_time_start = patch.value as ProviderBatchApplyUpdates["active_time_start"];
+        return { ok: true, data: undefined };
+      case "active_time_end":
+        updates.active_time_end = patch.value as ProviderBatchApplyUpdates["active_time_end"];
+        return { ok: true, data: undefined };
       case "preserve_client_ip":
         updates.preserve_client_ip = patch.value as ProviderBatchApplyUpdates["preserve_client_ip"];
         return { ok: true, data: undefined };
@@ -813,6 +834,13 @@ function applyPatchField<T>(
     case "anthropic_adaptive_thinking":
       updates.anthropic_adaptive_thinking = null;
       return { ok: true, data: undefined };
+    // Routing - active time clear to null
+    case "active_time_start":
+      updates.active_time_start = null;
+      return { ok: true, data: undefined };
+    case "active_time_end":
+      updates.active_time_end = null;
+      return { ok: true, data: undefined };
     // Routing - preference fields clear to "inherit"
     case "cache_ttl_preference":
       updates.cache_ttl_preference = "inherit";
@@ -893,6 +921,8 @@ export function buildProviderBatchApplyUpdates(
     ["anthropic_thinking_budget_preference", patch.anthropic_thinking_budget_preference],
     ["anthropic_adaptive_thinking", patch.anthropic_adaptive_thinking],
     // Routing
+    ["active_time_start", patch.active_time_start],
+    ["active_time_end", patch.active_time_end],
     ["preserve_client_ip", patch.preserve_client_ip],
     ["group_priorities", patch.group_priorities],
     ["cache_ttl_preference", patch.cache_ttl_preference],
@@ -956,6 +986,8 @@ export function hasProviderBatchPatchChanges(patch: ProviderBatchPatch): boolean
     patch.anthropic_thinking_budget_preference.mode !== "no_change" ||
     patch.anthropic_adaptive_thinking.mode !== "no_change" ||
     // Routing
+    patch.active_time_start.mode !== "no_change" ||
+    patch.active_time_end.mode !== "no_change" ||
     patch.preserve_client_ip.mode !== "no_change" ||
     patch.group_priorities.mode !== "no_change" ||
     patch.cache_ttl_preference.mode !== "no_change" ||

+ 79 - 0
src/lib/utils/provider-schedule.ts

@@ -0,0 +1,79 @@
+/**
+ * Provider Schedule Utilities
+ *
+ * Determines whether a provider is currently within its configured active time window.
+ * Supports same-day and cross-day (overnight) schedules using the system timezone.
+ */
+
+/**
+ * Check if a provider is currently active based on its schedule configuration.
+ *
+ * @param startTime - HH:mm format start time, or null (always active)
+ * @param endTime - HH:mm format end time, or null (always active)
+ * @param timezone - IANA timezone identifier (e.g., "Asia/Shanghai")
+ * @param now - Optional Date override for testing
+ * @returns true if the provider is currently active
+ *
+ * Rules:
+ * - Both null -> always active (true)
+ * - Either null -> always active (true) (defensive; validation ensures both-or-neither)
+ * - start === end -> false (zero-width window; validation blocks this input)
+ * - Same-day (start < end): start <= now < end
+ * - Cross-day (start > end): now >= start || now < end
+ */
+export function isProviderActiveNow(
+  startTime: string | null,
+  endTime: string | null,
+  timezone: string,
+  now: Date = new Date()
+): boolean {
+  if (startTime == null || endTime == null) {
+    return true;
+  }
+
+  if (startTime === endTime) {
+    return false;
+  }
+
+  const nowMinutes = getCurrentMinutesInTimezone(now, timezone);
+  const startMinutes = parseHHMM(startTime);
+  const endMinutes = parseHHMM(endTime);
+
+  // Fail-open: if DB contains malformed time values, treat provider as always active
+  if (Number.isNaN(startMinutes) || Number.isNaN(endMinutes)) {
+    return true;
+  }
+
+  if (startMinutes < endMinutes) {
+    // Same-day: start <= now < end
+    return nowMinutes >= startMinutes && nowMinutes < endMinutes;
+  }
+
+  // Cross-day: now >= start || now < end
+  return nowMinutes >= startMinutes || nowMinutes < endMinutes;
+}
+
+const HHMM_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
+
+function parseHHMM(time: string): number {
+  const match = HHMM_RE.exec(time);
+  if (!match) {
+    return Number.NaN;
+  }
+  return Number.parseInt(match[1], 10) * 60 + Number.parseInt(match[2], 10);
+}
+
+function getCurrentMinutesInTimezone(now: Date, timezone: string): number {
+  const formatter = new Intl.DateTimeFormat("en-US", {
+    timeZone: timezone,
+    hour: "2-digit",
+    minute: "2-digit",
+    hour12: false,
+  });
+
+  const parts = formatter.formatToParts(now);
+  const hour = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10);
+  const minute = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10);
+
+  return hour * 60 + minute;
+}

+ 55 - 0
src/lib/validation/schemas.ts

@@ -448,6 +448,17 @@ export const CreateProviderSchema = z
       .default("claude"),
     preserve_client_ip: z.boolean().optional().default(false),
     model_redirects: z.record(z.string(), z.string()).nullable().optional(),
+    // Scheduled active time window (HH:mm format)
+    active_time_start: z
+      .string()
+      .regex(/^([01][0-9]|2[0-3]):[0-5][0-9]$/, "active_time_start must be HH:mm format")
+      .nullable()
+      .optional(),
+    active_time_end: z
+      .string()
+      .regex(/^([01][0-9]|2[0-3]):[0-5][0-9]$/, "active_time_end must be HH:mm format")
+      .nullable()
+      .optional(),
     allowed_models: z.array(z.string()).nullable().optional(),
     allowed_clients: OPTIONAL_CLIENT_PATTERN_ARRAY_WITH_DEFAULT_SCHEMA,
     blocked_clients: OPTIONAL_CLIENT_PATTERN_ARRAY_WITH_DEFAULT_SCHEMA,
@@ -621,6 +632,23 @@ export const CreateProviderSchema = z
         });
       }
     }
+    // active_time_start and active_time_end must be both set or both null
+    const hasStart = data.active_time_start != null;
+    const hasEnd = data.active_time_end != null;
+    if (hasStart !== hasEnd) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: "active_time_start and active_time_end must be both set or both cleared",
+        path: [hasStart ? "active_time_end" : "active_time_start"],
+      });
+    }
+    if (hasStart && hasEnd && data.active_time_start === data.active_time_end) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: "active_time_start and active_time_end must not be the same",
+        path: ["active_time_end"],
+      });
+    }
   });
 
 /**
@@ -656,6 +684,16 @@ export const UpdateProviderSchema = z
       .optional(),
     preserve_client_ip: z.boolean().optional(),
     model_redirects: z.record(z.string(), z.string()).nullable().optional(),
+    active_time_start: z
+      .string()
+      .regex(/^([01][0-9]|2[0-3]):[0-5][0-9]$/, "active_time_start must be HH:mm format")
+      .nullable()
+      .optional(),
+    active_time_end: z
+      .string()
+      .regex(/^([01][0-9]|2[0-3]):[0-5][0-9]$/, "active_time_end must be HH:mm format")
+      .nullable()
+      .optional(),
     allowed_models: z.array(z.string()).nullable().optional(),
     allowed_clients: OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA,
     blocked_clients: OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA,
@@ -824,6 +862,23 @@ export const UpdateProviderSchema = z
         });
       }
     }
+    // active_time_start and active_time_end must be both set or both null
+    const hasStart = data.active_time_start != null;
+    const hasEnd = data.active_time_end != null;
+    if (hasStart !== hasEnd) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: "active_time_start and active_time_end must be both set or both cleared",
+        path: [hasStart ? "active_time_end" : "active_time_start"],
+      });
+    }
+    if (hasStart && hasEnd && data.active_time_start === data.active_time_end) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: "active_time_start and active_time_end must not be the same",
+        path: ["active_time_end"],
+      });
+    }
   });
 
 /**

+ 2 - 0
src/repository/_shared/transformers.ts

@@ -92,6 +92,8 @@ export function toProvider(dbProvider: any): Provider {
     providerType: dbProvider?.providerType ?? "claude",
     preserveClientIp: dbProvider?.preserveClientIp ?? false,
     modelRedirects: dbProvider?.modelRedirects ?? null,
+    activeTimeStart: dbProvider?.activeTimeStart ?? null,
+    activeTimeEnd: dbProvider?.activeTimeEnd ?? null,
     mcpPassthroughType: dbProvider?.mcpPassthroughType ?? "none",
     mcpPassthroughUrl: dbProvider?.mcpPassthroughUrl ?? null,
     limit5hUsd: dbProvider?.limit5hUsd ? parseFloat(dbProvider.limit5hUsd) : null,

+ 24 - 0
src/repository/provider.ts

@@ -187,6 +187,8 @@ export async function createProvider(providerData: CreateProviderData): Promise<
     allowedModels: providerData.allowed_models,
     allowedClients: providerData.allowed_clients ?? [],
     blockedClients: providerData.blocked_clients ?? [],
+    activeTimeStart: providerData.active_time_start ?? null,
+    activeTimeEnd: providerData.active_time_end ?? null,
     mcpPassthroughType: providerData.mcp_passthrough_type ?? "none",
     mcpPassthroughUrl: providerData.mcp_passthrough_url ?? null,
     limit5hUsd: providerData.limit_5h_usd != null ? providerData.limit_5h_usd.toString() : null,
@@ -264,6 +266,8 @@ export async function createProvider(providerData: CreateProviderData): Promise<
         allowedModels: providers.allowedModels,
         allowedClients: providers.allowedClients,
         blockedClients: providers.blockedClients,
+        activeTimeStart: providers.activeTimeStart,
+        activeTimeEnd: providers.activeTimeEnd,
         mcpPassthroughType: providers.mcpPassthroughType,
         mcpPassthroughUrl: providers.mcpPassthroughUrl,
         limit5hUsd: providers.limit5hUsd,
@@ -346,6 +350,8 @@ export async function findProviderList(
       allowedModels: providers.allowedModels,
       allowedClients: providers.allowedClients,
       blockedClients: providers.blockedClients,
+      activeTimeStart: providers.activeTimeStart,
+      activeTimeEnd: providers.activeTimeEnd,
       mcpPassthroughType: providers.mcpPassthroughType,
       mcpPassthroughUrl: providers.mcpPassthroughUrl,
       limit5hUsd: providers.limit5hUsd,
@@ -428,6 +434,8 @@ export async function findAllProvidersFresh(): Promise<Provider[]> {
       allowedModels: providers.allowedModels,
       allowedClients: providers.allowedClients,
       blockedClients: providers.blockedClients,
+      activeTimeStart: providers.activeTimeStart,
+      activeTimeEnd: providers.activeTimeEnd,
       mcpPassthroughType: providers.mcpPassthroughType,
       mcpPassthroughUrl: providers.mcpPassthroughUrl,
       limit5hUsd: providers.limit5hUsd,
@@ -514,6 +522,8 @@ export async function findProviderById(id: number): Promise<Provider | null> {
       allowedModels: providers.allowedModels,
       allowedClients: providers.allowedClients,
       blockedClients: providers.blockedClients,
+      activeTimeStart: providers.activeTimeStart,
+      activeTimeEnd: providers.activeTimeEnd,
       mcpPassthroughType: providers.mcpPassthroughType,
       mcpPassthroughUrl: providers.mcpPassthroughUrl,
       limit5hUsd: providers.limit5hUsd,
@@ -596,6 +606,10 @@ export async function updateProvider(
     dbData.allowedClients = providerData.allowed_clients ?? [];
   if (providerData.blocked_clients !== undefined)
     dbData.blockedClients = providerData.blocked_clients ?? [];
+  if (providerData.active_time_start !== undefined)
+    dbData.activeTimeStart = providerData.active_time_start ?? null;
+  if (providerData.active_time_end !== undefined)
+    dbData.activeTimeEnd = providerData.active_time_end ?? null;
   if (providerData.mcp_passthrough_type !== undefined)
     dbData.mcpPassthroughType = providerData.mcp_passthrough_type;
   if (providerData.mcp_passthrough_url !== undefined)
@@ -743,6 +757,8 @@ export async function updateProvider(
         allowedModels: providers.allowedModels,
         allowedClients: providers.allowedClients,
         blockedClients: providers.blockedClients,
+        activeTimeStart: providers.activeTimeStart,
+        activeTimeEnd: providers.activeTimeEnd,
         mcpPassthroughType: providers.mcpPassthroughType,
         mcpPassthroughUrl: providers.mcpPassthroughUrl,
         limit5hUsd: providers.limit5hUsd,
@@ -998,6 +1014,8 @@ export interface BatchProviderUpdates {
   anthropicAdaptiveThinking?: AnthropicAdaptiveThinkingConfig | null;
   // Routing
   preserveClientIp?: boolean;
+  activeTimeStart?: string | null;
+  activeTimeEnd?: string | null;
   groupPriorities?: Record<string, number> | null;
   cacheTtlPreference?: string | null;
   swapCacheTtlBilling?: boolean;
@@ -1082,6 +1100,12 @@ export async function updateProvidersBatch(
   if (updates.preserveClientIp !== undefined) {
     setClauses.preserveClientIp = updates.preserveClientIp;
   }
+  if (updates.activeTimeStart !== undefined) {
+    setClauses.activeTimeStart = updates.activeTimeStart;
+  }
+  if (updates.activeTimeEnd !== undefined) {
+    setClauses.activeTimeEnd = updates.activeTimeEnd;
+  }
   if (updates.groupPriorities !== undefined) {
     setClauses.groupPriorities = updates.groupPriorities;
   }

+ 1 - 0
src/types/message.ts

@@ -172,6 +172,7 @@ export interface ProviderChainItem {
         | "type_mismatch"
         | "model_not_allowed"
         | "context_1m_disabled" // 供应商禁用了 1M 上下文功能
+        | "schedule_inactive" // 供应商不在调度时间窗口内
         | "disabled"
         | "client_restriction"; // Provider filtered due to client restriction
       details?: string; // 额外信息(如费用:$15.2/$15)

+ 23 - 4
src/types/provider.ts

@@ -69,7 +69,9 @@ export type ProviderBatchPatchField =
   | "blocked_clients"
   | "anthropic_thinking_budget_preference"
   | "anthropic_adaptive_thinking"
-  // Routing
+  // Routing / Schedule
+  | "active_time_start"
+  | "active_time_end"
   | "preserve_client_ip"
   | "group_priorities"
   | "cache_ttl_preference"
@@ -118,7 +120,9 @@ export interface ProviderBatchPatchDraft {
   blocked_clients?: ProviderPatchDraftInput<string[]>;
   anthropic_thinking_budget_preference?: ProviderPatchDraftInput<AnthropicThinkingBudgetPreference>;
   anthropic_adaptive_thinking?: ProviderPatchDraftInput<AnthropicAdaptiveThinkingConfig>;
-  // Routing
+  // Routing / Schedule
+  active_time_start?: ProviderPatchDraftInput<string>;
+  active_time_end?: ProviderPatchDraftInput<string>;
   preserve_client_ip?: ProviderPatchDraftInput<boolean>;
   group_priorities?: ProviderPatchDraftInput<Record<string, number>>;
   cache_ttl_preference?: ProviderPatchDraftInput<CacheTtlPreference>;
@@ -168,7 +172,9 @@ export interface ProviderBatchPatch {
   blocked_clients: ProviderPatchOperation<string[]>;
   anthropic_thinking_budget_preference: ProviderPatchOperation<AnthropicThinkingBudgetPreference>;
   anthropic_adaptive_thinking: ProviderPatchOperation<AnthropicAdaptiveThinkingConfig>;
-  // Routing
+  // Routing / Schedule
+  active_time_start: ProviderPatchOperation<string>;
+  active_time_end: ProviderPatchOperation<string>;
   preserve_client_ip: ProviderPatchOperation<boolean>;
   group_priorities: ProviderPatchOperation<Record<string, number>>;
   cache_ttl_preference: ProviderPatchOperation<CacheTtlPreference>;
@@ -218,7 +224,9 @@ export interface ProviderBatchApplyUpdates {
   blocked_clients?: string[];
   anthropic_thinking_budget_preference?: AnthropicThinkingBudgetPreference | null;
   anthropic_adaptive_thinking?: AnthropicAdaptiveThinkingConfig | null;
-  // Routing
+  // Routing / Schedule
+  active_time_start?: string | null;
+  active_time_end?: string | null;
   preserve_client_ip?: boolean;
   group_priorities?: Record<string, number> | null;
   cache_ttl_preference?: CacheTtlPreference | null;
@@ -288,6 +296,10 @@ export interface Provider {
   preserveClientIp: boolean;
   modelRedirects: Record<string, string> | null;
 
+  // Scheduled active time window (HH:mm format, null = always active)
+  activeTimeStart: string | null;
+  activeTimeEnd: string | null;
+
   // 模型列表:双重语义
   // - Anthropic 提供商:白名单(管理员限制可调度的模型,可选)
   // - 非 Anthropic 提供商:声明列表(提供商声称支持的模型,可选)
@@ -398,6 +410,9 @@ export interface ProviderDisplay {
   // 是否透传客户端 IP
   preserveClientIp: boolean;
   modelRedirects: Record<string, string> | null;
+  // Scheduled active time window
+  activeTimeStart: string | null;
+  activeTimeEnd: string | null;
   // 模型列表(双重语义)
   allowedModels: string[] | null;
   allowedClients: string[]; // Allowed client patterns (empty = no restriction)
@@ -490,6 +505,8 @@ export interface CreateProviderData {
   provider_type?: ProviderType;
   preserve_client_ip?: boolean;
   model_redirects?: Record<string, string> | null;
+  active_time_start?: string | null;
+  active_time_end?: string | null;
   allowed_models?: string[] | null;
   allowed_clients?: string[] | null;
   blocked_clients?: string[] | null;
@@ -566,6 +583,8 @@ export interface UpdateProviderData {
   provider_type?: ProviderType;
   preserve_client_ip?: boolean;
   model_redirects?: Record<string, string> | null;
+  active_time_start?: string | null;
+  active_time_end?: string | null;
   allowed_models?: string[] | null;
   allowed_clients?: string[] | null;
   blocked_clients?: string[] | null;

+ 121 - 0
tests/unit/actions/providers-patch-contract.test.ts

@@ -909,6 +909,122 @@ describe("provider patch contract", () => {
 
       expect(hasProviderBatchPatchChanges(normalized.data)).toBe(false);
     });
+
+    it("detects change on active_time_start", () => {
+      const normalized = normalizeProviderBatchPatchDraft({
+        active_time_start: { set: "09:00" },
+      });
+
+      expect(normalized.ok).toBe(true);
+      if (!normalized.ok) return;
+
+      expect(hasProviderBatchPatchChanges(normalized.data)).toBe(true);
+    });
+
+    it("detects change on active_time_end", () => {
+      const normalized = normalizeProviderBatchPatchDraft({
+        active_time_end: { set: "17:00" },
+      });
+
+      expect(normalized.ok).toBe(true);
+      if (!normalized.ok) return;
+
+      expect(hasProviderBatchPatchChanges(normalized.data)).toBe(true);
+    });
+  });
+
+  describe("active_time_start / active_time_end batch patch", () => {
+    it("accepts active_time_start as string and maps to apply payload", () => {
+      const result = prepareProviderBatchApplyUpdates({
+        active_time_start: { set: "09:00" },
+      });
+
+      expect(result.ok).toBe(true);
+      if (!result.ok) return;
+
+      expect(result.data.active_time_start).toBe("09:00");
+    });
+
+    it("clears active_time_start to null", () => {
+      const result = prepareProviderBatchApplyUpdates({
+        active_time_start: { clear: true },
+      });
+
+      expect(result.ok).toBe(true);
+      if (!result.ok) return;
+
+      expect(result.data.active_time_start).toBeNull();
+    });
+
+    it("accepts active_time_end as string and maps to apply payload", () => {
+      const result = prepareProviderBatchApplyUpdates({
+        active_time_end: { set: "17:00" },
+      });
+
+      expect(result.ok).toBe(true);
+      if (!result.ok) return;
+
+      expect(result.data.active_time_end).toBe("17:00");
+    });
+
+    it("clears active_time_end to null", () => {
+      const result = prepareProviderBatchApplyUpdates({
+        active_time_end: { clear: true },
+      });
+
+      expect(result.ok).toBe(true);
+      if (!result.ok) return;
+
+      expect(result.data.active_time_end).toBeNull();
+    });
+
+    it("rejects non-string value for active_time_start", () => {
+      const result = normalizeProviderBatchPatchDraft({
+        active_time_start: { set: 900 } as never,
+      });
+
+      expect(result.ok).toBe(false);
+      if (result.ok) return;
+
+      expect(result.error.code).toBe(PROVIDER_PATCH_ERROR_CODES.INVALID_PATCH_SHAPE);
+      expect(result.error.field).toBe("active_time_start");
+    });
+
+    it("rejects non-string value for active_time_end", () => {
+      const result = normalizeProviderBatchPatchDraft({
+        active_time_end: { set: 900 } as never,
+      });
+
+      expect(result.ok).toBe(false);
+      if (result.ok) return;
+
+      expect(result.error.code).toBe(PROVIDER_PATCH_ERROR_CODES.INVALID_PATCH_SHAPE);
+      expect(result.error.field).toBe("active_time_end");
+    });
+
+    it("rejects invalid HH:mm format for active_time_start", () => {
+      const result = normalizeProviderBatchPatchDraft({
+        active_time_start: { set: "9:00" },
+      });
+
+      expect(result.ok).toBe(false);
+      if (result.ok) return;
+
+      expect(result.error.code).toBe(PROVIDER_PATCH_ERROR_CODES.INVALID_PATCH_SHAPE);
+      expect(result.error.field).toBe("active_time_start");
+    });
+
+    it("rejects out-of-range time for active_time_end", () => {
+      const result = normalizeProviderBatchPatchDraft({
+        active_time_end: { set: "25:00" },
+      });
+
+      expect(result.ok).toBe(false);
+      if (result.ok) return;
+
+      expect(result.error.code).toBe(PROVIDER_PATCH_ERROR_CODES.INVALID_PATCH_SHAPE);
+      expect(result.error.field).toBe("active_time_end");
+    });
   });
 
   describe("combined set across all categories", () => {
@@ -936,6 +1052,9 @@ describe("provider patch contract", () => {
         // mcp
         mcp_passthrough_type: { set: "minimax" },
         mcp_passthrough_url: { set: "https://api.minimaxi.com" },
+        // schedule
+        active_time_start: { set: "09:00" },
+        active_time_end: { set: "17:00" },
       });
 
       expect(result.ok).toBe(true);
@@ -957,6 +1076,8 @@ describe("provider patch contract", () => {
       expect(result.data.first_byte_timeout_streaming_ms).toBe(15000);
       expect(result.data.mcp_passthrough_type).toBe("minimax");
       expect(result.data.mcp_passthrough_url).toBe("https://api.minimaxi.com");
+      expect(result.data.active_time_start).toBe("09:00");
+      expect(result.data.active_time_end).toBe("17:00");
     });
   });
 });

+ 178 - 0
tests/unit/lib/utils/provider-schedule.test.ts

@@ -0,0 +1,178 @@
+import { describe, expect, it } from "vitest";
+import { isProviderActiveNow } from "@/lib/utils/provider-schedule";
+
+describe("isProviderActiveNow", () => {
+  // Helper: create a Date at a specific time in a given timezone
+  function makeDate(hh: number, mm: number, timezone: string): Date {
+    // Build a date string in the target timezone, then convert back to UTC
+    const now = new Date();
+    const year = now.getFullYear();
+    const month = now.getMonth();
+    const day = now.getDate();
+
+    // Create a date formatter for the target timezone
+    const formatter = new Intl.DateTimeFormat("en-US", {
+      timeZone: timezone,
+      year: "numeric",
+      month: "2-digit",
+      day: "2-digit",
+      hour: "2-digit",
+      minute: "2-digit",
+      second: "2-digit",
+      hour12: false,
+    });
+
+    // Get what the current time is in that timezone
+    const parts = formatter.formatToParts(now);
+    const getPart = (type: string) => parts.find((p) => p.type === type)?.value ?? "0";
+    const currentHourInTz = parseInt(getPart("hour"), 10);
+    const currentMinuteInTz = parseInt(getPart("minute"), 10);
+
+    // Compute the offset in ms we need to shift
+    const targetMinutes = hh * 60 + mm;
+    const currentMinutes = currentHourInTz * 60 + currentMinuteInTz;
+    const diffMs = (targetMinutes - currentMinutes) * 60 * 1000;
+
+    return new Date(now.getTime() + diffMs);
+  }
+
+  describe("null/undefined inputs (always active)", () => {
+    it("returns true when both start and end are null", () => {
+      expect(isProviderActiveNow(null, null, "UTC")).toBe(true);
+    });
+
+    it("returns true when start is null and end is non-null", () => {
+      expect(isProviderActiveNow(null, "18:00", "UTC")).toBe(true);
+    });
+
+    it("returns true when start is non-null and end is null", () => {
+      expect(isProviderActiveNow("09:00", null, "UTC")).toBe(true);
+    });
+  });
+
+  describe("same-day schedule (start < end)", () => {
+    const cases = [
+      { start: "09:00", end: "17:00", hour: 9, min: 0, expected: true, desc: "at start boundary" },
+      { start: "09:00", end: "17:00", hour: 12, min: 30, expected: true, desc: "middle of window" },
+      { start: "09:00", end: "17:00", hour: 16, min: 59, expected: true, desc: "just before end" },
+      {
+        start: "09:00",
+        end: "17:00",
+        hour: 17,
+        min: 0,
+        expected: false,
+        desc: "at end boundary (exclusive)",
+      },
+      {
+        start: "09:00",
+        end: "17:00",
+        hour: 8,
+        min: 59,
+        expected: false,
+        desc: "just before start",
+      },
+      { start: "09:00", end: "17:00", hour: 23, min: 0, expected: false, desc: "well after end" },
+      { start: "09:00", end: "17:00", hour: 0, min: 0, expected: false, desc: "midnight" },
+      { start: "00:00", end: "23:59", hour: 12, min: 0, expected: true, desc: "nearly full day" },
+    ];
+
+    for (const { start, end, hour, min, expected, desc } of cases) {
+      it(`${desc}: ${start}-${end} at ${String(hour).padStart(2, "0")}:${String(min).padStart(2, "0")} -> ${expected}`, () => {
+        const now = makeDate(hour, min, "UTC");
+        expect(isProviderActiveNow(start, end, "UTC", now)).toBe(expected);
+      });
+    }
+  });
+
+  describe("cross-day schedule (start > end)", () => {
+    const cases = [
+      { start: "22:00", end: "08:00", hour: 22, min: 0, expected: true, desc: "at start boundary" },
+      { start: "22:00", end: "08:00", hour: 23, min: 30, expected: true, desc: "late night" },
+      { start: "22:00", end: "08:00", hour: 0, min: 0, expected: true, desc: "midnight" },
+      { start: "22:00", end: "08:00", hour: 3, min: 0, expected: true, desc: "early morning" },
+      { start: "22:00", end: "08:00", hour: 7, min: 59, expected: true, desc: "just before end" },
+      {
+        start: "22:00",
+        end: "08:00",
+        hour: 8,
+        min: 0,
+        expected: false,
+        desc: "at end boundary (exclusive)",
+      },
+      { start: "22:00", end: "08:00", hour: 12, min: 0, expected: false, desc: "midday" },
+      {
+        start: "22:00",
+        end: "08:00",
+        hour: 21,
+        min: 59,
+        expected: false,
+        desc: "just before start",
+      },
+    ];
+
+    for (const { start, end, hour, min, expected, desc } of cases) {
+      it(`${desc}: ${start}-${end} at ${String(hour).padStart(2, "0")}:${String(min).padStart(2, "0")} -> ${expected}`, () => {
+        const now = makeDate(hour, min, "UTC");
+        expect(isProviderActiveNow(start, end, "UTC", now)).toBe(expected);
+      });
+    }
+  });
+
+  describe("edge cases", () => {
+    it("start === end returns false (zero-width window)", () => {
+      const now = makeDate(22, 0, "UTC");
+      expect(isProviderActiveNow("22:00", "22:00", "UTC", now)).toBe(false);
+    });
+
+    it("start === end returns false even at different time", () => {
+      const now = makeDate(10, 0, "UTC");
+      expect(isProviderActiveNow("22:00", "22:00", "UTC", now)).toBe(false);
+    });
+  });
+
+  describe("timezone support", () => {
+    it("same UTC time yields different results in different timezones", () => {
+      // At UTC 06:00, in Asia/Shanghai (UTC+8) it's 14:00
+      // Schedule 09:00-17:00 should be active in Shanghai but not in UTC
+      const utcDate = makeDate(6, 0, "UTC");
+
+      // In UTC at 06:00 with schedule 09:00-17:00 -> inactive
+      expect(isProviderActiveNow("09:00", "17:00", "UTC", utcDate)).toBe(false);
+
+      // In Asia/Shanghai at 14:00 with schedule 09:00-17:00 -> active
+      expect(isProviderActiveNow("09:00", "17:00", "Asia/Shanghai", utcDate)).toBe(true);
+    });
+  });
+
+  describe("malformed input defense", () => {
+    it("returns true (fail-open) for malformed start time", () => {
+      const now = makeDate(12, 0, "UTC");
+      expect(isProviderActiveNow("garbage", "17:00", "UTC", now)).toBe(true);
+    });
+
+    it("returns true (fail-open) for malformed end time", () => {
+      const now = makeDate(12, 0, "UTC");
+      expect(isProviderActiveNow("09:00", "not-a-time", "UTC", now)).toBe(true);
+    });
+
+    it("returns true (fail-open) for both times malformed", () => {
+      const now = makeDate(12, 0, "UTC");
+      expect(isProviderActiveNow("bad", "worse", "UTC", now)).toBe(true);
+    });
+
+    it("returns true (fail-open) for out-of-range hour (24:00)", () => {
+      const now = makeDate(12, 0, "UTC");
+      expect(isProviderActiveNow("24:00", "17:00", "UTC", now)).toBe(true);
+    });
+
+    it("returns true (fail-open) for single-digit hour (9:00)", () => {
+      const now = makeDate(12, 0, "UTC");
+      expect(isProviderActiveNow("9:00", "17:00", "UTC", now)).toBe(true);
+    });
+
+    it("returns true (fail-open) for out-of-range minutes (99:99)", () => {
+      const now = makeDate(12, 0, "UTC");
+      expect(isProviderActiveNow("99:99", "17:00", "UTC", now)).toBe(true);
+    });
+  });
+});