Просмотр исходного кода

feat(proxy): client restriction refactor - blockedClients + Claude Code sub-client detection (#812)

* feat(db): add blocked_clients to users and client restriction fields to providers

* feat(proxy): add client-detector module with multi-signal Claude Code detection

- BUILTIN_CLIENT_KEYWORDS set (7 keywords)
- 3-of-3 signal detection: x-app, UA prefix, betas array
- Sub-client extraction via ENTRYPOINT_MAP
- matchClientPattern: builtin keyword path + custom UA substring path
- isClientAllowed: blacklist-before-whitelist logic
- 48 unit tests, all passing

* feat(types,schema,validation): add client restriction fields across foundation layer

- types/user.ts: blockedClients in 5 interfaces
- types/provider.ts: allowedClients/blockedClients in Provider, ProviderDisplay, batch types
- types/message.ts: client_restriction_filtered in ProviderChainItem.reason, client_restriction in filteredProviders.reason
- drizzle/schema.ts: blocked_clients on users, allowed_clients/blocked_clients on providers
- validation/schemas.ts: blockedClients in user schemas, allowed_clients/blocked_clients in provider schemas
- actions/providers.ts: thread new fields through
- lib/provider-patch-contract.ts: add allowed_clients/blocked_clients to patch contract

* feat(proxy,repository): Wave 2 - guard refactor, provider-selector Step 1.5, repository fields

- client-guard.ts: use isClientAllowed from client-detector, add blockedClients support
- provider-selector.ts: Step 1.5 client restriction pre-filter (neutral, no circuit breaker)
- repository/user.ts: blockedClients read/write
- repository/provider.ts: allowedClients/blockedClients read/write
- repository/_shared/transformers.ts: blockedClients in transformer
- types/provider.ts: additional batch type entries
- tests: 78 tests passing (guard + detector)

* feat(i18n,actions): i18n client restriction keys + thread blockedClients through actions

- messages/*/provider-chain.json: client_restriction in filterReasons/filterDetails, client_restriction_filtered in reasons
- messages/*/dashboard.json: 7 Claude Code sub-client labels in presetClients, blockedClients in userEditSection.fields
- actions/users.ts: blockedClients in all create/update calls
- actions/providers.ts: allowedClients/blockedClients in all create/update calls

* feat(ui): add neutral client_restriction_filtered display in provider chain popover

* feat(ui): rewrite access-restrictions-section with hierarchical Claude Code panel and blockedClients

- Rewrite access-restrictions-section.tsx with Allow/Block toggles for all 7 Claude Code sub-clients and 3 other presets (gemini-cli, factory-cli, codex-cli)
- Add blockedClients prop to AccessRestrictionsSectionProps and UserEditSectionProps
- Add custom blockedClients tag input alongside existing allowedClients tag input
- Update use-user-translations with blockedClients i18n fields and all claude-code preset translations
- Wire blockedClients through create-user-dialog and edit-user-dialog

* chore: format code (feat-client-restriction-refactor-c3c48c2)

* feat(ui): add client restriction fields to provider form routing section

- Add allowedClients/blockedClients to RoutingState and action types
- Initialize and handle new fields in provider form context/reducer
- Render two TagInput fields in routing section for client restrictions
- Map new fields to API payload in form submission
- Add i18n keys for all 5 languages (en, zh-CN, zh-TW, ja, ru)

* fix(i18n): use halfwidth parentheses in ja/dashboard.json for Claude Code sub-client labels

* refactor(client-restrictions): extract shared preset logic with alias support

* refactor(validation): deduplicate client pattern schemas and add blockedClients permission

* feat(providers): add preset allow/block toggles to provider routing client restrictions

* feat(dashboard): unify user client restrictions with shared preset component

* docs(i18n): add client restriction preset labels and help text for all locales

* fix(proxy): address bugbot comments on client restriction logic

- Remove `as any` cast in confirmClaudeCodeSignals; use dot access on Record<string, unknown>
- Extract `normalize` to module-level constant; eliminates duplicate inline definitions
- Pre-compute Claude Code signals once in isClientAllowed to avoid per-pattern redundancy;
  guard with early return when both lists are empty to avoid unnecessary work
- Only require User-Agent when allowedClients is configured; blocklist-only requests with
  no UA now pass through correctly instead of returning 400
- Add provider-level client restriction check in findReusable() to prevent stale sessions
  from bypassing restrictions added after binding
- Disambiguate claude-code-cli builtin label from claude-cli UA-match in all 5 locales

* feat(proxy): add detailed client restriction visibility to decision chain

Replace generic "client_restriction" filter reason with specific match
context (blocklist_hit/allowlist_miss, matched pattern, detected client).
Session reuse path now records a chain item when rejecting a provider.
Client guard returns actionable error messages showing which pattern
blocked or which allowlist was missed. Dashboard and text timeline
render the new context details for auditing.

* fix(proxy): address bugbot comments on batch patch null safety and empty pattern validation

- Add allowed_clients/blocked_clients to PATCH_FIELD_CLEAR_VALUE with [] default
  to prevent null being written to NOT NULL columns during batch clear
- Add null coalescing in mapApplyUpdatesToRepositoryFormat for client fields
- Enforce non-empty client patterns with .trim().min(1) in CLIENT_PATTERN_SCHEMA

* perf(db): add covering indexes and time-bounded aggregation for slow queries

- Add 3 covering indexes on usage_ledger (user/key/provider + created_at + cost_usd)
  to enable index-only scans for SUM(cost_usd) aggregation queries
- Add composite index on message_request (session_id, created_at, user_id, key)
  for LATERAL JOIN session user-info lookup
- Replace idx_usage_ledger_key_cost to include created_at for time range filtering
- Add maxAgeDays=365 time boundary to sumUserTotalCostBatch,
  sumKeyTotalCostBatchByIds, and sumLedgerTotalCostBatch to avoid full table scans
- Refactor sumKeyTotalCostBatchByIds to two-step PK lookup + direct aggregate,
  eliminating varchar LEFT JOIN
- Rewrite aggregateMultipleSessionStats step 4 with LATERAL JOIN (1 index probe
  per session instead of returning all rows and deduplicating in JS)

Addresses 12-15s query regression on usage_ledger after migration from
message_request, and 1.5s session lookup on message_request.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Ding 1 месяц назад
Родитель
Сommit
658dbc3d3c
60 измененных файлов с 9658 добавлено и 299 удалено
  1. 3 0
      drizzle/0074_wide_retro_girl.sql
  2. 5 0
      drizzle/0075_faithful_speed_demon.sql
  3. 3723 0
      drizzle/meta/0074_snapshot.json
  4. 3819 0
      drizzle/meta/0075_snapshot.json
  5. 14 0
      drizzle/meta/_journal.json
  6. 21 2
      messages/en/dashboard.json
  7. 12 3
      messages/en/provider-chain.json
  8. 19 0
      messages/en/settings/providers/form/sections.json
  9. 21 2
      messages/ja/dashboard.json
  10. 12 3
      messages/ja/provider-chain.json
  11. 19 0
      messages/ja/settings/providers/form/sections.json
  12. 21 2
      messages/ru/dashboard.json
  13. 12 3
      messages/ru/provider-chain.json
  14. 19 0
      messages/ru/settings/providers/form/sections.json
  15. 21 2
      messages/zh-CN/dashboard.json
  16. 12 3
      messages/zh-CN/provider-chain.json
  17. 19 0
      messages/zh-CN/settings/providers/form/sections.json
  18. 21 2
      messages/zh-TW/dashboard.json
  19. 12 3
      messages/zh-TW/provider-chain.json
  20. 19 0
      messages/zh-TW/settings/providers/form/sections.json
  21. 24 0
      src/actions/providers.ts
  22. 12 0
      src/actions/users.ts
  23. 4 0
      src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx
  24. 4 0
      src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx
  25. 120 65
      src/app/[locale]/dashboard/_components/user/forms/access-restrictions-section.tsx
  26. 16 0
      src/app/[locale]/dashboard/_components/user/forms/user-edit-section.tsx
  27. 47 95
      src/app/[locale]/dashboard/_components/user/forms/user-form.tsx
  28. 25 6
      src/app/[locale]/dashboard/_components/user/hooks/use-user-translations.ts
  29. 83 0
      src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx
  30. 10 0
      src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx
  31. 2 0
      src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
  32. 10 0
      src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
  33. 4 0
      src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
  34. 127 0
      src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
  35. 236 0
      src/app/v1/_lib/proxy/client-detector.ts
  36. 18 39
      src/app/v1/_lib/proxy/client-guard.ts
  37. 96 1
      src/app/v1/_lib/proxy/provider-selector.ts
  38. 2 1
      src/app/v1/_lib/proxy/session.ts
  39. 28 1
      src/drizzle/schema.ts
  40. 50 0
      src/lib/client-restrictions/client-presets.test.ts
  41. 89 0
      src/lib/client-restrictions/client-presets.ts
  42. 1 1
      src/lib/database-backup/docker-executor.ts
  43. 1 0
      src/lib/permissions/user-field-permissions.ts
  44. 28 0
      src/lib/provider-patch-contract.ts
  45. 37 1
      src/lib/utils/provider-chain-formatter.ts
  46. 56 1
      src/lib/validation/schemas.test.ts
  47. 25 9
      src/lib/validation/schemas.ts
  48. 1 0
      src/repository/_shared/transformers.ts
  49. 56 26
      src/repository/message.ts
  50. 24 0
      src/repository/provider.ts
  51. 48 20
      src/repository/statistics.ts
  52. 12 3
      src/repository/usage-ledger.ts
  53. 8 0
      src/repository/user.ts
  54. 11 2
      src/types/message.ts
  55. 16 0
      src/types/provider.ts
  56. 5 0
      src/types/user.ts
  57. 428 0
      tests/unit/proxy/client-detector.test.ts
  58. 51 2
      tests/unit/proxy/client-guard.test.ts
  59. 6 0
      tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx
  60. 13 1
      tests/unit/user-dialogs.test.tsx

+ 3 - 0
drizzle/0074_wide_retro_girl.sql

@@ -0,0 +1,3 @@
+ALTER TABLE "providers" ADD COLUMN "allowed_clients" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
+ALTER TABLE "providers" ADD COLUMN "blocked_clients" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
+ALTER TABLE "users" ADD COLUMN "blocked_clients" jsonb DEFAULT '[]'::jsonb NOT NULL;

+ 5 - 0
drizzle/0075_faithful_speed_demon.sql

@@ -0,0 +1,5 @@
+DROP INDEX IF EXISTS "idx_usage_ledger_key_cost";--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS "idx_message_request_session_user_info" ON "message_request" USING btree ("session_id","created_at","user_id","key") WHERE "message_request"."deleted_at" IS NULL;--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS "idx_usage_ledger_user_cost_cover" ON "usage_ledger" USING btree ("user_id","created_at","cost_usd") WHERE "usage_ledger"."blocked_by" IS NULL;--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS "idx_usage_ledger_provider_cost_cover" ON "usage_ledger" USING btree ("final_provider_id","created_at","cost_usd") WHERE "usage_ledger"."blocked_by" IS NULL;--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS "idx_usage_ledger_key_cost" ON "usage_ledger" USING btree ("key","created_at","cost_usd") WHERE "usage_ledger"."blocked_by" IS NULL;

+ 3723 - 0
drizzle/meta/0074_snapshot.json

@@ -0,0 +1,3723 @@
+{
+  "id": "132bc4b6-86f3-43e1-b980-5ff62835bd1d",
+  "prevId": "0a7b1169-a126-4a17-a3e5-ce96ffcb0d59",
+  "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": {}
+        }
+      },
+      "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
+        },
+        "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"
+        },
+        "join_claude_pool": {
+          "name": "join_claude_pool",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 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": "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"
+      ]
+    },
+    "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": {}
+  }
+}

+ 3819 - 0
drizzle/meta/0075_snapshot.json

@@ -0,0 +1,3819 @@
+{
+  "id": "61c4da35-57cf-4629-88de-a1af77c8ae3b",
+  "prevId": "132bc4b6-86f3-43e1-b980-5ff62835bd1d",
+  "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
+        },
+        "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"
+        },
+        "join_claude_pool": {
+          "name": "join_claude_pool",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 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"
+      ]
+    },
+    "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": {}
+  }
+}

+ 14 - 0
drizzle/meta/_journal.json

@@ -519,6 +519,20 @@
       "when": 1771527016184,
       "tag": "0073_magical_manta",
       "breakpoints": true
+    },
+    {
+      "idx": 74,
+      "version": "7",
+      "when": 1771600203231,
+      "tag": "0074_wide_retro_girl",
+      "breakpoints": true
+    },
+    {
+      "idx": 75,
+      "version": "7",
+      "when": 1771688588623,
+      "tag": "0075_faithful_speed_demon",
+      "breakpoints": true
     }
   ]
 }

+ 21 - 2
messages/en/dashboard.json

@@ -1824,7 +1824,15 @@
           "label": "Client Restrictions",
           "description": "Restrict which CLI/IDE clients can use this account. Empty = no restriction.",
           "customLabel": "Custom Client Pattern",
-          "customPlaceholder": "Enter pattern (e.g., 'xcode', 'my-ide')"
+          "customPlaceholder": "Enter pattern (e.g., 'xcode', 'my-ide')",
+          "customHelp": "Custom patterns match User-Agent by case-insensitive substring. '-' and '_' are treated as equivalent."
+        },
+        "blockedClients": {
+          "label": "Blocked Clients",
+          "description": "Clients matching these patterns will be rejected, even if they match allowed clients.",
+          "customLabel": "Custom Block Pattern",
+          "customPlaceholder": "Enter pattern (e.g., 'xcode', 'my-ide')",
+          "customHelp": "Custom patterns match User-Agent by case-insensitive substring. '-' and '_' are treated as equivalent."
         },
         "allowedModels": {
           "label": "Model Restrictions",
@@ -1845,11 +1853,22 @@
           "processing": "Processing..."
         }
       },
+      "actions": {
+        "allow": "Allow",
+        "block": "Block"
+      },
       "presetClients": {
         "claude-cli": "Claude Code CLI",
         "gemini-cli": "Gemini CLI",
         "factory-cli": "Droid CLI",
-        "codex-cli": "Codex CLI"
+        "codex-cli": "Codex CLI",
+        "claude-code": "Claude Code (all)",
+        "claude-code-cli": "Claude Code CLI (builtin)",
+        "claude-code-cli-sdk": "Claude Code CLI SDK",
+        "claude-code-vscode": "Claude Code VSCode",
+        "claude-code-sdk-ts": "Claude Code SDK (TypeScript)",
+        "claude-code-sdk-py": "Claude Code SDK (Python)",
+        "claude-code-gh-action": "Claude Code GitHub Action"
       }
     },
     "keyEditSection": {

+ 12 - 3
messages/en/provider-chain.json

@@ -55,7 +55,8 @@
     "session_reuse": "Session Reuse",
     "initial_selection": "Initial Selection",
     "endpoint_pool_exhausted": "Endpoint Pool Exhausted",
-    "vendor_type_all_timeout": "Vendor-Type All Endpoints Timeout"
+    "vendor_type_all_timeout": "Vendor-Type All Endpoints Timeout",
+    "client_restriction_filtered": "Client Restricted"
   },
   "filterReasons": {
     "rate_limited": "Rate Limited",
@@ -70,13 +71,21 @@
     "group_mismatch": "Group Mismatch",
     "health_check_failed": "Health Check Failed",
     "endpoint_circuit_open": "Endpoint Circuit Open",
-    "endpoint_disabled": "Endpoint Disabled"
+    "endpoint_disabled": "Endpoint Disabled",
+    "client_restriction": "Client Restriction"
   },
   "filterDetails": {
     "vendor_type_circuit_open": "Vendor-type temporarily circuit-broken",
     "circuit_open": "Circuit breaker open",
     "circuit_half_open": "Circuit breaker half-open",
-    "rate_limited": "Rate limited"
+    "rate_limited": "Rate limited",
+    "provider_client_restriction": "Provider skipped due to client restriction",
+    "session_reuse_client_restriction": "Session reuse rejected: client restriction",
+    "blocklist_hit": "Blocked by pattern: {pattern}",
+    "allowlist_miss": "Not in allowed list",
+    "detectedClient": "Detected: {client}",
+    "providerAllowlist": "Allowlist: {list}",
+    "providerBlocklist": "Blocklist: {list}"
   },
   "details": {
     "selectionMethod": "Selection",

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

@@ -305,6 +305,25 @@
       "selectedOnly": "Only the selected {count} models are allowed. Other models will not be routed to this provider.",
       "title": "Model Allowlist"
     },
+    "clientRestrictions": {
+      "allowedLabel": "Allowed Clients",
+      "allowedPlaceholder": "e.g. claude-code-cli",
+      "blockedLabel": "Blocked Clients",
+      "blockedPlaceholder": "e.g. gemini-cli",
+      "allowAction": "Allow",
+      "blockAction": "Block",
+      "customAllowedLabel": "Custom Allowed Patterns",
+      "customAllowedPlaceholder": "e.g. my-ide, internal-tool",
+      "customBlockedLabel": "Custom Blocked Patterns",
+      "customBlockedPlaceholder": "e.g. legacy-client",
+      "customHelp": "Custom patterns use case-insensitive User-Agent contains matching. '-' and '_' are treated as equivalent.",
+      "presetClients": {
+        "claude-code": "Claude Code (all)",
+        "gemini-cli": "Gemini CLI",
+        "factory-cli": "Droid CLI",
+        "codex-cli": "Codex CLI"
+      }
+    },
     "preserveClientIp": {
       "desc": "Pass x-forwarded-for / x-real-ip to upstream providers (may expose real client IP)",
       "help": "Keep off by default for privacy. Enable only when upstream must see the end-user IP.",

+ 21 - 2
messages/ja/dashboard.json

@@ -1760,7 +1760,15 @@
           "label": "クライアント制限",
           "description": "このアカウントを使用できるCLI/IDEクライアントを制限します。空欄は制限なし。",
           "customLabel": "カスタムクライアントパターン",
-          "customPlaceholder": "パターンを入力(例:'xcode', 'my-ide')"
+          "customPlaceholder": "パターンを入力(例:'xcode', 'my-ide')",
+          "customHelp": "カスタムパターンは User-Agent の部分一致で判定されます(大文字小文字を区別しません)。'-' と '_' は同等として扱います。"
+        },
+        "blockedClients": {
+          "label": "ブロックするクライアント",
+          "description": "これらのパターンに一致するクライアントは、許可リストに一致しても拒否されます。",
+          "customLabel": "カスタムブロックパターン",
+          "customPlaceholder": "パターンを入力(例: 'xcode'、'my-ide')",
+          "customHelp": "カスタムパターンは User-Agent の部分一致で判定されます(大文字小文字を区別しません)。'-' と '_' は同等として扱います。"
         },
         "allowedModels": {
           "label": "モデル制限",
@@ -1781,11 +1789,22 @@
           "processing": "処理中..."
         }
       },
+      "actions": {
+        "allow": "許可",
+        "block": "ブロック"
+      },
       "presetClients": {
         "claude-cli": "Claude Code CLI",
         "gemini-cli": "Gemini CLI",
         "factory-cli": "Droid CLI",
-        "codex-cli": "Codex CLI"
+        "codex-cli": "Codex CLI",
+        "claude-code": "Claude Code (全て)",
+        "claude-code-cli": "Claude Code CLI (厳密検出)",
+        "claude-code-cli-sdk": "Claude Code CLI SDK",
+        "claude-code-vscode": "Claude Code VSCode",
+        "claude-code-sdk-ts": "Claude Code SDK (TypeScript)",
+        "claude-code-sdk-py": "Claude Code SDK (Python)",
+        "claude-code-gh-action": "Claude Code GitHub Action"
       }
     },
     "keyEditSection": {

+ 12 - 3
messages/ja/provider-chain.json

@@ -55,7 +55,8 @@
     "session_reuse": "セッション再利用",
     "initial_selection": "初期選択",
     "endpoint_pool_exhausted": "エンドポイントプール枯渇",
-    "vendor_type_all_timeout": "ベンダータイプ全エンドポイントタイムアウト"
+    "vendor_type_all_timeout": "ベンダータイプ全エンドポイントタイムアウト",
+    "client_restriction_filtered": "クライアント制限"
   },
   "filterReasons": {
     "rate_limited": "レート制限",
@@ -70,13 +71,21 @@
     "group_mismatch": "グループ不一致",
     "health_check_failed": "ヘルスチェック失敗",
     "endpoint_circuit_open": "エンドポイントサーキットオープン",
-    "endpoint_disabled": "エンドポイント無効"
+    "endpoint_disabled": "エンドポイント無効",
+    "client_restriction": "クライアント制限"
   },
   "filterDetails": {
     "vendor_type_circuit_open": "ベンダータイプ一時サーキットブレイク",
     "circuit_open": "サーキットブレーカーオープン",
     "circuit_half_open": "サーキットブレーカーハーフオープン",
-    "rate_limited": "レート制限"
+    "rate_limited": "レート制限",
+    "provider_client_restriction": "クライアント制限によりプロバイダーをスキップ",
+    "session_reuse_client_restriction": "Session reuse rejected: client restriction",
+    "blocklist_hit": "Blocked by pattern: {pattern}",
+    "allowlist_miss": "Not in allowed list",
+    "detectedClient": "Detected: {client}",
+    "providerAllowlist": "Allowlist: {list}",
+    "providerBlocklist": "Blocklist: {list}"
   },
   "details": {
     "selectionMethod": "選択方法",

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

@@ -306,6 +306,25 @@
       "selectedOnly": "選択した {count} 件のモデルのみ許可します。他のモデルはこのプロバイダーにルーティングされません。",
       "title": "モデル許可リスト"
     },
+    "clientRestrictions": {
+      "allowedLabel": "許可クライアント",
+      "allowedPlaceholder": "例: claude-code-cli",
+      "blockedLabel": "ブロッククライアント",
+      "blockedPlaceholder": "例: gemini-cli",
+      "allowAction": "許可",
+      "blockAction": "ブロック",
+      "customAllowedLabel": "カスタム許可パターン",
+      "customAllowedPlaceholder": "例: my-ide、internal-tool",
+      "customBlockedLabel": "カスタムブロックパターン",
+      "customBlockedPlaceholder": "例: legacy-client",
+      "customHelp": "カスタムパターンは User-Agent の部分一致で判定されます(大文字小文字を区別しません)。'-' と '_' は同等として扱います。",
+      "presetClients": {
+        "claude-code": "Claude Code(すべて)",
+        "gemini-cli": "Gemini CLI",
+        "factory-cli": "Droid CLI",
+        "codex-cli": "Codex CLI"
+      }
+    },
     "preserveClientIp": {
       "desc": "x-forwarded-for / x-real-ip を上流に渡します(実際の IP が露出する可能性)",
       "help": "プライバシー保護のためデフォルトはオフ。上流側で端末 IP が必要な場合のみ有効化してください。",

+ 21 - 2
messages/ru/dashboard.json

@@ -1808,7 +1808,15 @@
           "label": "Ограничения клиентов",
           "description": "Ограничьте, какие CLI/IDE клиенты могут использовать эту учетную запись. Пусто = без ограничений.",
           "customLabel": "Пользовательские шаблоны клиентов",
-          "customPlaceholder": "Введите шаблон (например, 'xcode', 'my-ide')"
+          "customPlaceholder": "Введите шаблон (например, 'xcode', 'my-ide')",
+          "customHelp": "Пользовательские шаблоны проверяются по вхождению в User-Agent без учёта регистра. '-' и '_' считаются эквивалентными."
+        },
+        "blockedClients": {
+          "label": "Заблокированные клиенты",
+          "description": "Клиенты, соответствующие этим шаблонам, будут отклонены, даже если они соответствуют разрешённым.",
+          "customLabel": "Пользовательский шаблон блокировки",
+          "customPlaceholder": "Введите шаблон (например, 'xcode', 'my-ide')",
+          "customHelp": "Пользовательские шаблоны проверяются по вхождению в User-Agent без учёта регистра. '-' и '_' считаются эквивалентными."
         },
         "allowedModels": {
           "label": "Ограничения моделей",
@@ -1829,11 +1837,22 @@
           "processing": "Обработка..."
         }
       },
+      "actions": {
+        "allow": "Разрешить",
+        "block": "Блокировать"
+      },
       "presetClients": {
         "claude-cli": "Claude Code CLI",
         "gemini-cli": "Gemini CLI",
         "factory-cli": "Droid CLI",
-        "codex-cli": "Codex CLI"
+        "codex-cli": "Codex CLI",
+        "claude-code": "Claude Code (все)",
+        "claude-code-cli": "Claude Code CLI (точное обнаружение)",
+        "claude-code-cli-sdk": "Claude Code CLI SDK",
+        "claude-code-vscode": "Claude Code VSCode",
+        "claude-code-sdk-ts": "Claude Code SDK (TypeScript)",
+        "claude-code-sdk-py": "Claude Code SDK (Python)",
+        "claude-code-gh-action": "Claude Code GitHub Action"
       }
     },
     "keyEditSection": {

+ 12 - 3
messages/ru/provider-chain.json

@@ -55,7 +55,8 @@
     "session_reuse": "Повторное использование сессии",
     "initial_selection": "Первоначальный выбор",
     "endpoint_pool_exhausted": "Пул конечных точек исчерпан",
-    "vendor_type_all_timeout": "Тайм-аут всех конечных точек типа поставщика"
+    "vendor_type_all_timeout": "Тайм-аут всех конечных точек типа поставщика",
+    "client_restriction_filtered": "Клиент ограничен"
   },
   "filterReasons": {
     "rate_limited": "Ограничение скорости",
@@ -70,13 +71,21 @@
     "group_mismatch": "Несоответствие группы",
     "health_check_failed": "Проверка состояния не пройдена",
     "endpoint_circuit_open": "Автомат конечной точки открыт",
-    "endpoint_disabled": "Эндпоинт отключен"
+    "endpoint_disabled": "Эндпоинт отключен",
+    "client_restriction": "Ограничение клиента"
   },
   "filterDetails": {
     "vendor_type_circuit_open": "Временное размыкание типа поставщика",
     "circuit_open": "Размыкатель открыт",
     "circuit_half_open": "Размыкатель полуоткрыт",
-    "rate_limited": "Ограничение скорости"
+    "rate_limited": "Ограничение скорости",
+    "provider_client_restriction": "Провайдер пропущен из-за ограничения клиента",
+    "session_reuse_client_restriction": "Session reuse rejected: client restriction",
+    "blocklist_hit": "Blocked by pattern: {pattern}",
+    "allowlist_miss": "Not in allowed list",
+    "detectedClient": "Detected: {client}",
+    "providerAllowlist": "Allowlist: {list}",
+    "providerBlocklist": "Blocklist: {list}"
   },
   "details": {
     "selectionMethod": "Метод выбора",

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

@@ -306,6 +306,25 @@
       "selectedOnly": "Разрешены только выбранные {count} моделей. Другие модели не будут направляться к этому провайдеру.",
       "title": "Список разрешённых моделей"
     },
+    "clientRestrictions": {
+      "allowedLabel": "Разрешённые клиенты",
+      "allowedPlaceholder": "напр. claude-code-cli",
+      "blockedLabel": "Заблокированные клиенты",
+      "blockedPlaceholder": "напр. gemini-cli",
+      "allowAction": "Разрешить",
+      "blockAction": "Блокировать",
+      "customAllowedLabel": "Пользовательские разрешённые шаблоны",
+      "customAllowedPlaceholder": "напр. my-ide, internal-tool",
+      "customBlockedLabel": "Пользовательские шаблоны блокировки",
+      "customBlockedPlaceholder": "напр. legacy-client",
+      "customHelp": "Пользовательские шаблоны проверяются по вхождению в User-Agent без учёта регистра. '-' и '_' считаются эквивалентными.",
+      "presetClients": {
+        "claude-code": "Claude Code (все)",
+        "gemini-cli": "Gemini CLI",
+        "factory-cli": "Droid CLI",
+        "codex-cli": "Codex CLI"
+      }
+    },
     "preserveClientIp": {
       "desc": "Передавать x-forwarded-for / x-real-ip в апстрим (может раскрыть реальный IP клиента)",
       "help": "По умолчанию выключено для приватности. Включайте только если апстриму нужен IP пользователя.",

+ 21 - 2
messages/zh-CN/dashboard.json

@@ -1783,7 +1783,15 @@
           "label": "客户端限制",
           "description": "限制哪些 CLI/IDE 客户端可以使用此账户。留空表示无限制。",
           "customLabel": "自定义客户端模式",
-          "customPlaceholder": "输入自定义模式(如:'xcode', 'my-ide')"
+          "customPlaceholder": "输入自定义模式(如:'xcode', 'my-ide')",
+          "customHelp": "自定义模式按 User-Agent 包含匹配(不区分大小写),并将 '-' 与 '_' 视为等价。"
+        },
+        "blockedClients": {
+          "label": "黑名单客户端",
+          "description": "匹配这些模式的客户端将被拒绝,即使它们也匹配白名单。",
+          "customLabel": "自定义黑名单模式",
+          "customPlaceholder": "输入模式(如 'xcode'、'my-ide')",
+          "customHelp": "自定义模式按 User-Agent 包含匹配(不区分大小写),并将 '-' 与 '_' 视为等价。"
         },
         "allowedModels": {
           "label": "模型限制",
@@ -1804,11 +1812,22 @@
           "processing": "处理中..."
         }
       },
+      "actions": {
+        "allow": "允许",
+        "block": "阻止"
+      },
       "presetClients": {
         "claude-cli": "Claude Code CLI",
         "gemini-cli": "Gemini CLI",
         "factory-cli": "Droid CLI",
-        "codex-cli": "Codex CLI"
+        "codex-cli": "Codex CLI",
+        "claude-code": "Claude Code(全部)",
+        "claude-code-cli": "Claude Code CLI (精确检测)",
+        "claude-code-cli-sdk": "Claude Code CLI SDK",
+        "claude-code-vscode": "Claude Code VSCode",
+        "claude-code-sdk-ts": "Claude Code SDK(TypeScript)",
+        "claude-code-sdk-py": "Claude Code SDK(Python)",
+        "claude-code-gh-action": "Claude Code GitHub Action"
       }
     },
     "keyEditSection": {

+ 12 - 3
messages/zh-CN/provider-chain.json

@@ -55,7 +55,8 @@
     "session_reuse": "会话复用",
     "initial_selection": "首次选择",
     "endpoint_pool_exhausted": "端点池耗尽",
-    "vendor_type_all_timeout": "供应商类型全端点超时"
+    "vendor_type_all_timeout": "供应商类型全端点超时",
+    "client_restriction_filtered": "客户端受限"
   },
   "filterReasons": {
     "rate_limited": "速率限制",
@@ -70,13 +71,21 @@
     "group_mismatch": "分组不匹配",
     "health_check_failed": "健康检查失败",
     "endpoint_circuit_open": "端点已熔断",
-    "endpoint_disabled": "端点已禁用"
+    "endpoint_disabled": "端点已禁用",
+    "client_restriction": "客户端限制"
   },
   "filterDetails": {
     "vendor_type_circuit_open": "供应商类型临时熔断",
     "circuit_open": "熔断器打开",
     "circuit_half_open": "熔断器半开",
-    "rate_limited": "速率限制"
+    "rate_limited": "速率限制",
+    "provider_client_restriction": "供应商因客户端限制被跳过",
+    "session_reuse_client_restriction": "Session reuse rejected: client restriction",
+    "blocklist_hit": "Blocked by pattern: {pattern}",
+    "allowlist_miss": "Not in allowed list",
+    "detectedClient": "Detected: {client}",
+    "providerAllowlist": "Allowlist: {list}",
+    "providerBlocklist": "Blocklist: {list}"
   },
   "details": {
     "selectionMethod": "选择方式",

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

@@ -49,6 +49,25 @@
       "selectedOnly": "仅允许选中的 {count} 个模型。其他模型的请求不会调度到此供应商。",
       "moreModels": "+{count} 更多"
     },
+    "clientRestrictions": {
+      "allowedLabel": "白名单客户端",
+      "allowedPlaceholder": "例如 claude-code-cli",
+      "blockedLabel": "黑名单客户端",
+      "blockedPlaceholder": "例如 gemini-cli",
+      "allowAction": "允许",
+      "blockAction": "阻止",
+      "customAllowedLabel": "自定义白名单模式",
+      "customAllowedPlaceholder": "例如 my-ide、internal-tool",
+      "customBlockedLabel": "自定义黑名单模式",
+      "customBlockedPlaceholder": "例如 legacy-client",
+      "customHelp": "自定义模式使用 User-Agent 包含匹配(不区分大小写),并将 '-' 与 '_' 视为等价。",
+      "presetClients": {
+        "claude-code": "Claude Code(全部)",
+        "gemini-cli": "Gemini CLI",
+        "factory-cli": "Droid CLI",
+        "codex-cli": "Codex CLI"
+      }
+    },
     "scheduleParams": {
       "title": "调度参数",
       "priority": {

+ 21 - 2
messages/zh-TW/dashboard.json

@@ -1768,7 +1768,15 @@
           "label": "用戶端限制",
           "description": "限制哪些 CLI/IDE 用戶端可以使用此帳戶。留空表示無限制。",
           "customLabel": "自訂用戶端模式",
-          "customPlaceholder": "輸入自訂模式(如:'xcode', 'my-ide')"
+          "customPlaceholder": "輸入自訂模式(如:'xcode', 'my-ide')",
+          "customHelp": "自訂模式會以 User-Agent 包含比對(不區分大小寫),並將 '-' 與 '_' 視為等價。"
+        },
+        "blockedClients": {
+          "label": "黑名單客戶端",
+          "description": "符合這些模式的客戶端將被拒絕,即使它們也符合白名單。",
+          "customLabel": "自訂黑名單模式",
+          "customPlaceholder": "輸入模式(如 'xcode'、'my-ide')",
+          "customHelp": "自訂模式會以 User-Agent 包含比對(不區分大小寫),並將 '-' 與 '_' 視為等價。"
         },
         "allowedModels": {
           "label": "Model 限制",
@@ -1789,11 +1797,22 @@
           "processing": "處理中..."
         }
       },
+      "actions": {
+        "allow": "允許",
+        "block": "封鎖"
+      },
       "presetClients": {
         "claude-cli": "Claude Code CLI",
         "gemini-cli": "Gemini CLI",
         "factory-cli": "Droid CLI",
-        "codex-cli": "Codex CLI"
+        "codex-cli": "Codex CLI",
+        "claude-code": "Claude Code(全部)",
+        "claude-code-cli": "Claude Code CLI (精確檢測)",
+        "claude-code-cli-sdk": "Claude Code CLI SDK",
+        "claude-code-vscode": "Claude Code VSCode",
+        "claude-code-sdk-ts": "Claude Code SDK(TypeScript)",
+        "claude-code-sdk-py": "Claude Code SDK(Python)",
+        "claude-code-gh-action": "Claude Code GitHub Action"
       }
     },
     "keyEditSection": {

+ 12 - 3
messages/zh-TW/provider-chain.json

@@ -55,7 +55,8 @@
     "session_reuse": "會話複用",
     "initial_selection": "首次選擇",
     "endpoint_pool_exhausted": "端點池耗盡",
-    "vendor_type_all_timeout": "供應商類型全端點逾時"
+    "vendor_type_all_timeout": "供應商類型全端點逾時",
+    "client_restriction_filtered": "客戶端受限"
   },
   "filterReasons": {
     "rate_limited": "速率限制",
@@ -70,13 +71,21 @@
     "group_mismatch": "分組不匹配",
     "health_check_failed": "健康檢查失敗",
     "endpoint_circuit_open": "端點已熔斷",
-    "endpoint_disabled": "端點已停用"
+    "endpoint_disabled": "端點已停用",
+    "client_restriction": "客戶端限制"
   },
   "filterDetails": {
     "vendor_type_circuit_open": "供應商類型臨時熔斷",
     "circuit_open": "熔斷器打開",
     "circuit_half_open": "熔斷器半開",
-    "rate_limited": "速率限制"
+    "rate_limited": "速率限制",
+    "provider_client_restriction": "供應商因客戶端限制被跳過",
+    "session_reuse_client_restriction": "Session reuse rejected: client restriction",
+    "blocklist_hit": "Blocked by pattern: {pattern}",
+    "allowlist_miss": "Not in allowed list",
+    "detectedClient": "Detected: {client}",
+    "providerAllowlist": "Allowlist: {list}",
+    "providerBlocklist": "Blocklist: {list}"
   },
   "details": {
     "selectionMethod": "選擇方式",

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

@@ -306,6 +306,25 @@
       "selectedOnly": "僅允許所選的 {count} 個模型。其他模型將不會被路由到此供應商。",
       "title": "模型允許清單"
     },
+    "clientRestrictions": {
+      "allowedLabel": "白名單客戶端",
+      "allowedPlaceholder": "例如 claude-code-cli",
+      "blockedLabel": "黑名單客戶端",
+      "blockedPlaceholder": "例如 gemini-cli",
+      "allowAction": "允許",
+      "blockAction": "封鎖",
+      "customAllowedLabel": "自訂白名單模式",
+      "customAllowedPlaceholder": "例如 my-ide、internal-tool",
+      "customBlockedLabel": "自訂黑名單模式",
+      "customBlockedPlaceholder": "例如 legacy-client",
+      "customHelp": "自訂模式使用 User-Agent 包含比對(不區分大小寫),並將 '-' 與 '_' 視為等價。",
+      "presetClients": {
+        "claude-code": "Claude Code(全部)",
+        "gemini-cli": "Gemini CLI",
+        "factory-cli": "Droid CLI",
+        "codex-cli": "Codex CLI"
+      }
+    },
     "preserveClientIp": {
       "desc": "向上游轉發 x-forwarded-for / x-real-ip,可能暴露真實來源 IP",
       "help": "預設關閉以保護隱私;僅在需要上游感知終端 IP 時開啟。",

+ 24 - 0
src/actions/providers.ts

@@ -273,6 +273,8 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
         preserveClientIp: provider.preserveClientIp,
         modelRedirects: provider.modelRedirects,
         allowedModels: provider.allowedModels,
+        allowedClients: provider.allowedClients,
+        blockedClients: provider.blockedClients,
         mcpPassthroughType: provider.mcpPassthroughType,
         mcpPassthroughUrl: provider.mcpPassthroughUrl,
         limit5hUsd: provider.limit5hUsd,
@@ -477,6 +479,8 @@ export async function addProvider(data: {
   preserve_client_ip?: boolean;
   model_redirects?: Record<string, string> | null;
   allowed_models?: string[] | null;
+  allowed_clients?: string[] | null;
+  blocked_clients?: string[] | null;
   limit_5h_usd?: number | null;
   limit_daily_usd?: number | null;
   daily_reset_mode?: "fixed" | "rolling";
@@ -648,6 +652,8 @@ export async function editProvider(
     preserve_client_ip?: boolean;
     model_redirects?: Record<string, string> | null;
     allowed_models?: string[] | null;
+    allowed_clients?: string[] | null;
+    blocked_clients?: string[] | null;
     limit_5h_usd?: number | null;
     limit_daily_usd?: number | null;
     daily_reset_time?: string;
@@ -1399,6 +1405,12 @@ function mapApplyUpdatesToRepositoryFormat(
   if (applyUpdates.allowed_models !== undefined) {
     result.allowedModels = applyUpdates.allowed_models;
   }
+  if (applyUpdates.allowed_clients !== undefined) {
+    result.allowedClients = applyUpdates.allowed_clients ?? [];
+  }
+  if (applyUpdates.blocked_clients !== undefined) {
+    result.blockedClients = applyUpdates.blocked_clients ?? [];
+  }
   if (applyUpdates.anthropic_thinking_budget_preference !== undefined) {
     result.anthropicThinkingBudgetPreference = applyUpdates.anthropic_thinking_budget_preference;
   }
@@ -1512,6 +1524,8 @@ const PATCH_FIELD_TO_PROVIDER_KEY: Record<ProviderBatchPatchField, keyof Provide
   group_tag: "groupTag",
   model_redirects: "modelRedirects",
   allowed_models: "allowedModels",
+  allowed_clients: "allowedClients",
+  blocked_clients: "blockedClients",
   anthropic_thinking_budget_preference: "anthropicThinkingBudgetPreference",
   anthropic_adaptive_thinking: "anthropicAdaptiveThinking",
   preserve_client_ip: "preserveClientIp",
@@ -1547,6 +1561,8 @@ const PATCH_FIELD_TO_PROVIDER_KEY: Record<ProviderBatchPatchField, keyof Provide
 };
 
 const PATCH_FIELD_CLEAR_VALUE: Partial<Record<ProviderBatchPatchField, unknown>> = {
+  allowed_clients: [],
+  blocked_clients: [],
   anthropic_thinking_budget_preference: "inherit",
   cache_ttl_preference: "inherit",
   context_1m_preference: "inherit",
@@ -2031,6 +2047,8 @@ export interface BatchUpdateProvidersParams {
     group_tag?: string | null;
     model_redirects?: Record<string, string> | null;
     allowed_models?: string[] | null;
+    allowed_clients?: string[];
+    blocked_clients?: string[];
     anthropic_thinking_budget_preference?: AnthropicThinkingBudgetPreference | null;
     anthropic_adaptive_thinking?: AnthropicAdaptiveThinkingConfig | null;
   };
@@ -2079,6 +2097,12 @@ export async function batchUpdateProviders(
           ? null
           : updates.allowed_models;
     }
+    if (updates.allowed_clients !== undefined) {
+      repositoryUpdates.allowedClients = updates.allowed_clients;
+    }
+    if (updates.blocked_clients !== undefined) {
+      repositoryUpdates.blockedClients = updates.blocked_clients;
+    }
     if (updates.anthropic_thinking_budget_preference !== undefined) {
       repositoryUpdates.anthropicThinkingBudgetPreference =
         updates.anthropic_thinking_budget_preference;

+ 12 - 0
src/actions/users.ts

@@ -254,6 +254,7 @@ export async function getUsers(): Promise<UserDisplay[]> {
           isEnabled: user.isEnabled,
           expiresAt: user.expiresAt ?? null,
           allowedClients: user.allowedClients || [],
+          blockedClients: user.blockedClients || [],
           allowedModels: user.allowedModels ?? [],
           keys: keys.map((key) => {
             const stats = statisticsLookup.get(key.id);
@@ -320,6 +321,7 @@ export async function getUsers(): Promise<UserDisplay[]> {
           isEnabled: user.isEnabled,
           expiresAt: user.expiresAt ?? null,
           allowedClients: user.allowedClients || [],
+          blockedClients: user.blockedClients || [],
           allowedModels: user.allowedModels ?? [],
           keys: [],
         };
@@ -523,6 +525,7 @@ export async function getUsersBatch(
           isEnabled: user.isEnabled,
           expiresAt: user.expiresAt ?? null,
           allowedClients: user.allowedClients || [],
+          blockedClients: user.blockedClients || [],
           allowedModels: user.allowedModels ?? [],
           keys: keys.map((key) => {
             const stats = statisticsLookup.get(key.id);
@@ -585,6 +588,7 @@ export async function getUsersBatch(
           isEnabled: user.isEnabled,
           expiresAt: user.expiresAt ?? null,
           allowedClients: user.allowedClients || [],
+          blockedClients: user.blockedClients || [],
           allowedModels: user.allowedModels ?? [],
           keys: [],
         };
@@ -750,6 +754,7 @@ export async function addUser(data: {
   isEnabled?: boolean;
   expiresAt?: Date | null;
   allowedClients?: string[];
+  blockedClients?: string[];
   allowedModels?: string[];
 }): Promise<
   ActionResult<{
@@ -810,6 +815,7 @@ export async function addUser(data: {
       isEnabled: data.isEnabled,
       expiresAt: data.expiresAt,
       allowedClients: data.allowedClients || [],
+      blockedClients: data.blockedClients || [],
       allowedModels: data.allowedModels || [],
     });
 
@@ -869,6 +875,7 @@ export async function addUser(data: {
       isEnabled: validatedData.isEnabled,
       expiresAt: validatedData.expiresAt ?? null,
       allowedClients: validatedData.allowedClients ?? [],
+      blockedClients: validatedData.blockedClients ?? [],
       allowedModels: validatedData.allowedModels ?? [],
     });
 
@@ -942,6 +949,7 @@ export async function createUserOnly(data: {
   isEnabled?: boolean;
   expiresAt?: Date | null;
   allowedClients?: string[];
+  blockedClients?: string[];
   allowedModels?: string[];
 }): Promise<
   ActionResult<{
@@ -995,6 +1003,7 @@ export async function createUserOnly(data: {
       isEnabled: data.isEnabled,
       expiresAt: data.expiresAt,
       allowedClients: data.allowedClients || [],
+      blockedClients: data.blockedClients || [],
       allowedModels: data.allowedModels || [],
     });
 
@@ -1053,6 +1062,7 @@ export async function createUserOnly(data: {
       isEnabled: validatedData.isEnabled,
       expiresAt: validatedData.expiresAt ?? null,
       allowedClients: validatedData.allowedClients ?? [],
+      blockedClients: validatedData.blockedClients ?? [],
       allowedModels: validatedData.allowedModels ?? [],
     });
 
@@ -1111,6 +1121,7 @@ export async function editUser(
     isEnabled?: boolean;
     expiresAt?: Date | null;
     allowedClients?: string[];
+    blockedClients?: string[];
     allowedModels?: string[];
   }
 ): Promise<ActionResult> {
@@ -1211,6 +1222,7 @@ export async function editUser(
       isEnabled: validatedData.isEnabled,
       expiresAt: validatedData.expiresAt,
       allowedClients: validatedData.allowedClients,
+      blockedClients: validatedData.blockedClients,
       allowedModels: validatedData.allowedModels,
     });
 

+ 4 - 0
src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx

@@ -42,6 +42,7 @@ const CreateUserSchema = UpdateUserSchema.extend({
   name: z.string().min(1).max(64),
   providerGroup: z.string().max(200).nullable().optional(),
   allowedClients: z.array(z.string().max(64)).max(50).optional().default([]),
+  blockedClients: z.array(z.string().max(64)).max(50).optional().default([]),
   allowedModels: z.array(z.string().max(64)).max(50).optional().default([]),
   dailyQuota: z.number().nullable().optional(),
 });
@@ -89,6 +90,7 @@ function buildDefaultValues(): CreateFormValues {
       dailyResetMode: "fixed",
       dailyResetTime: "00:00",
       allowedClients: [],
+      blockedClients: [],
       allowedModels: [],
     },
     key: {
@@ -155,6 +157,7 @@ function CreateUserDialogInner({ onOpenChange, onSuccess }: CreateUserDialogProp
             dailyResetMode: data.user.dailyResetMode,
             dailyResetTime: data.user.dailyResetTime,
             allowedClients: data.user.allowedClients,
+            blockedClients: data.user.blockedClients,
             allowedModels: data.user.allowedModels,
           });
           if (!userRes.ok) {
@@ -363,6 +366,7 @@ function CreateUserDialogInner({ onOpenChange, onSuccess }: CreateUserDialogProp
               dailyResetMode: currentUserDraft.dailyResetMode ?? "fixed",
               dailyResetTime: currentUserDraft.dailyResetTime ?? "00:00",
               allowedClients: currentUserDraft.allowedClients || [],
+              blockedClients: currentUserDraft.blockedClients || [],
               allowedModels: currentUserDraft.allowedModels || [],
             }}
             isEnabled={true}

+ 4 - 0
src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx

@@ -50,6 +50,7 @@ const EditUserSchema = UpdateUserSchema.extend({
   name: z.string().min(1).max(64),
   providerGroup: z.string().max(200).nullable().optional(),
   allowedClients: z.array(z.string().max(64)).max(50).optional().default([]),
+  blockedClients: z.array(z.string().max(64)).max(50).optional().default([]),
   allowedModels: z.array(z.string().max(64)).max(50).optional().default([]),
   dailyQuota: z.number().nullable().optional(),
 });
@@ -73,6 +74,7 @@ function buildDefaultValues(user: UserDisplay): EditUserValues {
     dailyResetMode: user.dailyResetMode ?? "fixed",
     dailyResetTime: user.dailyResetTime ?? "00:00",
     allowedClients: user.allowedClients || [],
+    blockedClients: user.blockedClients || [],
     allowedModels: user.allowedModels || [],
   };
 }
@@ -113,6 +115,7 @@ function EditUserDialogInner({ onOpenChange, user, onSuccess }: EditUserDialogPr
             dailyResetMode: data.dailyResetMode,
             dailyResetTime: data.dailyResetTime,
             allowedClients: data.allowedClients,
+            blockedClients: data.blockedClients,
             allowedModels: data.allowedModels,
           });
           if (!userRes.ok) {
@@ -270,6 +273,7 @@ function EditUserDialogInner({ onOpenChange, user, onSuccess }: EditUserDialogPr
               dailyResetMode: currentUserDraft.dailyResetMode ?? "fixed",
               dailyResetTime: currentUserDraft.dailyResetTime ?? "00:00",
               allowedClients: currentUserDraft.allowedClients || [],
+              blockedClients: currentUserDraft.blockedClients || [],
               allowedModels: currentUserDraft.allowedModels || [],
             }}
             isEnabled={user.isEnabled}

+ 120 - 65
src/app/[locale]/dashboard/_components/user/forms/access-restrictions-section.tsx

@@ -5,24 +5,24 @@ import { useCallback, useMemo } from "react";
 import { ArrayTagInputField } from "@/components/form/form-field";
 import { Checkbox } from "@/components/ui/checkbox";
 import { Label } from "@/components/ui/label";
+import {
+  CLIENT_RESTRICTION_PRESET_OPTIONS,
+  isPresetSelected,
+  mergePresetAndCustomClients,
+  removePresetValues,
+  splitPresetAndCustomClients,
+  togglePresetSelection,
+} from "@/lib/client-restrictions/client-presets";
 
-// Preset client patterns
-const PRESET_CLIENTS = [
-  { value: "claude-cli", label: "Claude Code CLI" },
-  { value: "gemini-cli", label: "Gemini CLI" },
-  { value: "factory-cli", label: "Droid CLI" },
-  { value: "codex-cli", label: "Codex CLI" },
-];
-
-// Model name validation pattern: allows alphanumeric, dots, colons, slashes, underscores, hyphens
-// Examples: gemini-1.5-pro, gpt-4.1, claude-3-opus-20240229, o1-mini
+// Model name validation pattern
 const MODEL_NAME_PATTERN = /^[a-zA-Z0-9._:/-]+$/;
 
 export interface AccessRestrictionsSectionProps {
   allowedClients: string[];
+  blockedClients: string[];
   allowedModels: string[];
   modelSuggestions: string[];
-  onChange: (field: "allowedClients" | "allowedModels", value: string[]) => void;
+  onChange: (field: "allowedClients" | "blockedClients" | "allowedModels", value: string[]) => void;
   translations: {
     sections: {
       accessRestrictions: string;
@@ -33,6 +33,14 @@ export interface AccessRestrictionsSectionProps {
         description: string;
         customLabel: string;
         customPlaceholder: string;
+        customHelp: string;
+      };
+      blockedClients: {
+        label: string;
+        description: string;
+        customLabel: string;
+        customPlaceholder: string;
+        customHelp: string;
       };
       allowedModels: {
         label: string;
@@ -40,102 +48,149 @@ export interface AccessRestrictionsSectionProps {
         description: string;
       };
     };
+    actions: {
+      allow: string;
+      block: string;
+    };
     presetClients: Record<string, string>;
   };
 }
 
 export function AccessRestrictionsSection({
   allowedClients,
+  blockedClients,
   allowedModels,
   modelSuggestions,
   onChange,
   translations,
 }: AccessRestrictionsSectionProps) {
-  // Separate preset clients from custom clients
-  const { presetSelected, customClients } = useMemo(() => {
-    const presetValues = PRESET_CLIENTS.map((p) => p.value);
-    const preset = (allowedClients || []).filter((c) => presetValues.includes(c));
-    const custom = (allowedClients || []).filter((c) => !presetValues.includes(c));
-    return { presetSelected: preset, customClients: custom };
-  }, [allowedClients]);
-
-  const handlePresetChange = (clientValue: string, checked: boolean) => {
-    const currentClients = allowedClients || [];
+  const allowed = allowedClients || [];
+  const blocked = blockedClients || [];
+
+  const { customValues: customAllowed } = useMemo(
+    () => splitPresetAndCustomClients(allowed),
+    [allowed]
+  );
+
+  const { customValues: customBlocked } = useMemo(
+    () => splitPresetAndCustomClients(blocked),
+    [blocked]
+  );
+
+  const handleAllowToggle = (presetValue: string, checked: boolean) => {
+    onChange("allowedClients", togglePresetSelection(allowed, presetValue, checked));
+    if (checked) {
+      onChange("blockedClients", removePresetValues(blocked, presetValue));
+    }
+  };
+
+  const handleBlockToggle = (presetValue: string, checked: boolean) => {
+    onChange("blockedClients", togglePresetSelection(blocked, presetValue, checked));
     if (checked) {
-      onChange("allowedClients", [...currentClients, clientValue]);
-    } else {
-      onChange(
-        "allowedClients",
-        currentClients.filter((c) => c !== clientValue)
-      );
+      onChange("allowedClients", removePresetValues(allowed, presetValue));
     }
   };
 
-  const handleCustomClientsChange = (newCustomClients: string[]) => {
-    // Merge preset clients with new custom clients
-    onChange("allowedClients", [...presetSelected, ...newCustomClients]);
+  const handleCustomAllowedChange = (newCustom: string[]) => {
+    onChange("allowedClients", mergePresetAndCustomClients(allowed, newCustom));
+  };
+
+  const handleCustomBlockedChange = (newCustom: string[]) => {
+    onChange("blockedClients", mergePresetAndCustomClients(blocked, newCustom));
   };
 
-  // Custom validation for model names (allows dots, colons, slashes)
   const validateModelTag = useCallback(
     (tag: string): boolean => {
       if (!tag || tag.trim().length === 0) return false;
       if (tag.length > 64) return false;
       if (!MODEL_NAME_PATTERN.test(tag)) return false;
-      if (allowedModels.includes(tag)) return false; // duplicate check
-      if (allowedModels.length >= 50) return false; // max tags check
+      if (allowedModels.includes(tag)) return false;
+      if (allowedModels.length >= 50) return false;
       return true;
     },
     [allowedModels]
   );
 
+  const renderPresetRow = (value: string) => {
+    const isAllowed = isPresetSelected(allowed, value);
+    const isBlocked = isPresetSelected(blocked, value);
+    const displayLabel = translations.presetClients[value] ?? value;
+
+    return (
+      <div key={value} className="flex items-center gap-4 py-1">
+        <span className="text-sm flex-1 text-foreground">{displayLabel}</span>
+        <div className="flex items-center gap-3">
+          <div className="flex items-center gap-1.5">
+            <Checkbox
+              id={`allow-${value}`}
+              checked={isAllowed}
+              onCheckedChange={(checked) => handleAllowToggle(value, checked === true)}
+            />
+            <Label
+              htmlFor={`allow-${value}`}
+              className="text-xs font-normal cursor-pointer text-muted-foreground"
+            >
+              {translations.actions.allow}
+            </Label>
+          </div>
+          <div className="flex items-center gap-1.5">
+            <Checkbox
+              id={`block-${value}`}
+              checked={isBlocked}
+              onCheckedChange={(checked) => handleBlockToggle(value, checked === true)}
+            />
+            <Label
+              htmlFor={`block-${value}`}
+              className="text-xs font-normal cursor-pointer text-muted-foreground"
+            >
+              {translations.actions.block}
+            </Label>
+          </div>
+        </div>
+      </div>
+    );
+  };
+
   return (
-    <section className="rounded-lg border border-border bg-card/50 p-3 space-y-3">
-      <div className="flex items-center gap-2">
-        <Shield className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
-        <h4 className="text-sm font-semibold">{translations.sections.accessRestrictions}</h4>
+    <section className="space-y-4">
+      <div className="flex items-center gap-2 mb-2">
+        <Shield className="h-4 w-4 text-muted-foreground" />
+        <h3 className="text-sm font-medium">{translations.sections.accessRestrictions}</h3>
       </div>
 
-      {/* Allowed Clients (CLI/IDE restrictions) */}
+      {/* Client Restrictions */}
       <div className="space-y-3">
-        <div className="space-y-0.5">
-          <Label className="text-sm font-medium">{translations.fields.allowedClients.label}</Label>
-          <p className="text-xs text-muted-foreground">
+        <div>
+          <p className="text-sm font-medium mb-1">{translations.fields.allowedClients.label}</p>
+          <p className="text-xs text-muted-foreground mb-2">
             {translations.fields.allowedClients.description}
           </p>
         </div>
 
-        {/* Preset client checkboxes in 2x2 grid */}
-        <div className="grid grid-cols-2 gap-2">
-          {PRESET_CLIENTS.map((client) => {
-            const isChecked = presetSelected.includes(client.value);
-            const displayLabel = translations.presetClients[client.value] || client.label;
-            return (
-              <div key={client.value} className="flex items-center space-x-2">
-                <Checkbox
-                  id={`client-${client.value}`}
-                  checked={isChecked}
-                  onCheckedChange={(checked) => handlePresetChange(client.value, checked === true)}
-                />
-                <Label
-                  htmlFor={`client-${client.value}`}
-                  className="text-sm font-normal cursor-pointer"
-                >
-                  {displayLabel}
-                </Label>
-              </div>
-            );
-          })}
+        <div className="space-y-0.5 border rounded-md p-2">
+          {CLIENT_RESTRICTION_PRESET_OPTIONS.map((client) => renderPresetRow(client.value))}
         </div>
 
-        {/* Custom client patterns */}
+        {/* Custom allowed patterns */}
         <ArrayTagInputField
           label={translations.fields.allowedClients.customLabel}
+          description={translations.fields.allowedClients.customHelp}
           maxTagLength={64}
           maxTags={50}
           placeholder={translations.fields.allowedClients.customPlaceholder}
-          value={customClients}
-          onChange={handleCustomClientsChange}
+          value={customAllowed}
+          onChange={handleCustomAllowedChange}
+        />
+
+        {/* Custom blocked patterns */}
+        <ArrayTagInputField
+          label={translations.fields.blockedClients.customLabel}
+          description={translations.fields.blockedClients.customHelp}
+          maxTagLength={64}
+          maxTags={50}
+          placeholder={translations.fields.blockedClients.customPlaceholder}
+          value={customBlocked}
+          onChange={handleCustomBlockedChange}
         />
       </div>
 

+ 16 - 0
src/app/[locale]/dashboard/_components/user/forms/user-edit-section.tsx

@@ -45,6 +45,7 @@ export interface UserEditSectionProps {
     dailyResetTime?: string;
     // 访问限制字段
     allowedClients?: string[];
+    blockedClients?: string[];
     allowedModels?: string[];
   };
   isEnabled?: boolean;
@@ -99,6 +100,14 @@ export interface UserEditSectionProps {
         description: string;
         customLabel: string;
         customPlaceholder: string;
+        customHelp: string;
+      };
+      blockedClients: {
+        label: string;
+        description: string;
+        customLabel: string;
+        customPlaceholder: string;
+        customHelp: string;
       };
       allowedModels: {
         label: string;
@@ -106,6 +115,10 @@ export interface UserEditSectionProps {
         description: string;
       };
     };
+    actions: {
+      allow: string;
+      block: string;
+    };
     presetClients: Record<string, string>;
     limitRules: {
       addRule: string;
@@ -481,6 +494,7 @@ export function UserEditSection({
 
       <AccessRestrictionsSection
         allowedClients={user.allowedClients || []}
+        blockedClients={user.blockedClients || []}
         allowedModels={user.allowedModels || []}
         modelSuggestions={modelSuggestions}
         onChange={onChange}
@@ -490,8 +504,10 @@ export function UserEditSection({
           },
           fields: {
             allowedClients: translations.fields.allowedClients,
+            blockedClients: translations.fields.blockedClients,
             allowedModels: translations.fields.allowedModels,
           },
+          actions: translations.actions,
           presetClients: translations.presetClients,
         }}
       />

+ 47 - 95
src/app/[locale]/dashboard/_components/user/forms/user-form.tsx

@@ -9,9 +9,7 @@ import { addUser, editUser } from "@/actions/users";
 import { DatePickerField } from "@/components/form/date-picker-field";
 import { ArrayTagInputField, TagInputField, TextField } from "@/components/form/form-field";
 import { DialogFormLayout, FormGrid } from "@/components/form/form-layout";
-import { Checkbox } from "@/components/ui/checkbox";
 import { InlineWarning } from "@/components/ui/inline-warning";
-import { Label } from "@/components/ui/label";
 import { Switch } from "@/components/ui/switch";
 import { PROVIDER_GROUP } from "@/lib/constants/provider.constants";
 import { USER_LIMITS } from "@/lib/constants/user.constants";
@@ -20,14 +18,7 @@ import { formatDateToLocalYmd, parseYmdToLocalEndOfDay } from "@/lib/utils/date-
 import { getErrorMessage } from "@/lib/utils/error-messages";
 import { setZodErrorMap } from "@/lib/utils/zod-i18n";
 import { CreateUserSchema } from "@/lib/validation/schemas";
-
-// Preset client patterns
-const PRESET_CLIENTS = [
-  { value: "claude-cli", label: "Claude Code CLI" },
-  { value: "gemini-cli", label: "Gemini CLI" },
-  { value: "factory-cli", label: "Droid CLI" },
-  { value: "codex-cli", label: "Codex CLI" },
-];
+import { AccessRestrictionsSection } from "./access-restrictions-section";
 
 // 前端表单使用的 schema(接受字符串日期)
 const UserFormSchema = CreateUserSchema.extend({
@@ -51,6 +42,7 @@ interface UserFormProps {
     isEnabled?: boolean;
     expiresAt?: Date | null;
     allowedClients?: string[];
+    blockedClients?: string[];
     allowedModels?: string[];
   };
   onSuccess?: () => void;
@@ -103,6 +95,7 @@ export function UserForm({ user, onSuccess, currentUser }: UserFormProps) {
       isEnabled: user?.isEnabled ?? true,
       expiresAt: user?.expiresAt ? formatDateToLocalYmd(user.expiresAt) : "",
       allowedClients: user?.allowedClients || [],
+      blockedClients: user?.blockedClients || [],
       allowedModels: user?.allowedModels || [],
     },
     onSubmit: async (data) => {
@@ -131,6 +124,7 @@ export function UserForm({ user, onSuccess, currentUser }: UserFormProps) {
               isEnabled: data.isEnabled,
               expiresAt,
               allowedClients: data.allowedClients,
+              blockedClients: data.blockedClients,
               allowedModels: data.allowedModels,
             });
           } else {
@@ -149,6 +143,7 @@ export function UserForm({ user, onSuccess, currentUser }: UserFormProps) {
               isEnabled: data.isEnabled,
               expiresAt,
               allowedClients: data.allowedClients,
+              blockedClients: data.blockedClients,
               allowedModels: data.allowedModels,
             });
           }
@@ -176,6 +171,7 @@ export function UserForm({ user, onSuccess, currentUser }: UserFormProps) {
 
   // Use dashboard translations for form
   const tForm = useTranslations("dashboard.userForm");
+  const tUserEdit = useTranslations("dashboard.userManagement.userEditSection");
 
   const expiresAtPastWarning = useMemo(() => {
     const expiresAtYmd = form.values.expiresAt ?? "";
@@ -363,92 +359,48 @@ export function UserForm({ user, onSuccess, currentUser }: UserFormProps) {
           />
           {expiresAtPastWarning && <InlineWarning>{expiresAtPastWarning}</InlineWarning>}
 
-          {/* Allowed Clients (CLI/IDE restrictions) */}
-          <div className="space-y-3">
-            <div className="space-y-0.5">
-              <Label className="text-sm font-medium">{tForm("allowedClients.label")}</Label>
-              <p className="text-xs text-muted-foreground">{tForm("allowedClients.description")}</p>
-            </div>
-
-            {/* Preset client checkboxes */}
-            <div className="grid grid-cols-2 gap-2">
-              {PRESET_CLIENTS.map((client) => {
-                const isChecked = (form.values.allowedClients || []).includes(client.value);
-                return (
-                  <div key={client.value} className="flex items-center space-x-2">
-                    <Checkbox
-                      id={`client-${client.value}`}
-                      checked={isChecked}
-                      onCheckedChange={(checked) => {
-                        const currentClients = form.values.allowedClients || [];
-                        if (checked) {
-                          form.setValue("allowedClients", [...currentClients, client.value]);
-                        } else {
-                          form.setValue(
-                            "allowedClients",
-                            currentClients.filter((c: string) => c !== client.value)
-                          );
-                        }
-                      }}
-                    />
-                    <Label
-                      htmlFor={`client-${client.value}`}
-                      className="text-sm font-normal cursor-pointer"
-                    >
-                      {client.label}
-                    </Label>
-                  </div>
-                );
-              })}
-            </div>
-
-            {/* Custom client patterns */}
-            <ArrayTagInputField
-              label={tForm("allowedClients.customLabel")}
-              maxTagLength={64}
-              maxTags={50}
-              placeholder={tForm("allowedClients.customPlaceholder")}
-              onInvalidTag={(_tag, reason) => {
-                const messages: Record<string, string> = {
-                  empty: tUI("emptyTag"),
-                  duplicate: tUI("duplicateTag"),
-                  too_long: tUI("tooLong", { max: 64 }),
-                  invalid_format: tUI("invalidFormat"),
-                  max_tags: tUI("maxTags"),
-                };
-                toast.error(messages[reason] || reason);
-              }}
-              value={(form.values.allowedClients || []).filter(
-                (c: string) => !PRESET_CLIENTS.some((p) => p.value === c)
-              )}
-              onChange={(customClients: string[]) => {
-                // Merge preset clients with custom clients
-                const presetClients = (form.values.allowedClients || []).filter((c: string) =>
-                  PRESET_CLIENTS.some((p) => p.value === c)
-                );
-                form.setValue("allowedClients", [...presetClients, ...customClients]);
-              }}
-            />
-          </div>
-
-          {/* Allowed Models (AI model restrictions) */}
-          <ArrayTagInputField
-            label={tForm("allowedModels.label")}
-            maxTagLength={64}
-            maxTags={50}
-            placeholder={tForm("allowedModels.placeholder")}
-            description={tForm("allowedModels.description")}
-            onInvalidTag={(_tag, reason) => {
-              const messages: Record<string, string> = {
-                empty: tUI("emptyTag"),
-                duplicate: tUI("duplicateTag"),
-                too_long: tUI("tooLong", { max: 64 }),
-                invalid_format: tUI("invalidFormat"),
-                max_tags: tUI("maxTags"),
-              };
-              toast.error(messages[reason] || reason);
+          <AccessRestrictionsSection
+            allowedClients={form.values.allowedClients || []}
+            blockedClients={form.values.blockedClients || []}
+            allowedModels={form.values.allowedModels || []}
+            modelSuggestions={[]}
+            onChange={(field, value) => form.setValue(field, value)}
+            translations={{
+              sections: {
+                accessRestrictions: tUserEdit("sections.accessRestrictions"),
+              },
+              fields: {
+                allowedClients: {
+                  label: tUserEdit("fields.allowedClients.label"),
+                  description: tUserEdit("fields.allowedClients.description"),
+                  customLabel: tUserEdit("fields.allowedClients.customLabel"),
+                  customPlaceholder: tUserEdit("fields.allowedClients.customPlaceholder"),
+                  customHelp: tUserEdit("fields.allowedClients.customHelp"),
+                },
+                blockedClients: {
+                  label: tUserEdit("fields.blockedClients.label"),
+                  description: tUserEdit("fields.blockedClients.description"),
+                  customLabel: tUserEdit("fields.blockedClients.customLabel"),
+                  customPlaceholder: tUserEdit("fields.blockedClients.customPlaceholder"),
+                  customHelp: tUserEdit("fields.blockedClients.customHelp"),
+                },
+                allowedModels: {
+                  label: tUserEdit("fields.allowedModels.label"),
+                  placeholder: tUserEdit("fields.allowedModels.placeholder"),
+                  description: tUserEdit("fields.allowedModels.description"),
+                },
+              },
+              actions: {
+                allow: tUserEdit("actions.allow"),
+                block: tUserEdit("actions.block"),
+              },
+              presetClients: {
+                "claude-code": tUserEdit("presetClients.claude-code"),
+                "gemini-cli": tUserEdit("presetClients.gemini-cli"),
+                "factory-cli": tUserEdit("presetClients.factory-cli"),
+                "codex-cli": tUserEdit("presetClients.codex-cli"),
+              },
             }}
-            {...form.getArrayFieldProps("allowedModels")}
           />
         </>
       )}

+ 25 - 6
src/app/[locale]/dashboard/_components/user/hooks/use-user-translations.ts

@@ -56,6 +56,14 @@ export interface UserEditTranslations {
       description: string;
       customLabel: string;
       customPlaceholder: string;
+      customHelp: string;
+    };
+    blockedClients: {
+      label: string;
+      description: string;
+      customLabel: string;
+      customPlaceholder: string;
+      customHelp: string;
     };
     allowedModels: {
       label: string;
@@ -63,12 +71,11 @@ export interface UserEditTranslations {
       description: string;
     };
   };
-  presetClients: {
-    "claude-cli": string;
-    "gemini-cli": string;
-    "factory-cli": string;
-    "codex-cli": string;
+  actions: {
+    allow: string;
+    block: string;
   };
+  presetClients: Record<string, string>;
   limitRules: {
     addRule: string;
     ruleTypes: {
@@ -171,6 +178,14 @@ export function useUserTranslations(
           description: t("userEditSection.fields.allowedClients.description"),
           customLabel: t("userEditSection.fields.allowedClients.customLabel"),
           customPlaceholder: t("userEditSection.fields.allowedClients.customPlaceholder"),
+          customHelp: t("userEditSection.fields.allowedClients.customHelp"),
+        },
+        blockedClients: {
+          label: t("userEditSection.fields.blockedClients.label"),
+          description: t("userEditSection.fields.blockedClients.description"),
+          customLabel: t("userEditSection.fields.blockedClients.customLabel"),
+          customPlaceholder: t("userEditSection.fields.blockedClients.customPlaceholder"),
+          customHelp: t("userEditSection.fields.blockedClients.customHelp"),
         },
         allowedModels: {
           label: t("userEditSection.fields.allowedModels.label"),
@@ -178,8 +193,12 @@ export function useUserTranslations(
           description: t("userEditSection.fields.allowedModels.description"),
         },
       },
+      actions: {
+        allow: t("userEditSection.actions.allow"),
+        block: t("userEditSection.actions.block"),
+      },
       presetClients: {
-        "claude-cli": t("userEditSection.presetClients.claude-cli"),
+        "claude-code": t("userEditSection.presetClients.claude-code"),
         "gemini-cli": t("userEditSection.presetClients.gemini-cli"),
         "factory-cli": t("userEditSection.presetClients.factory-cli"),
         "codex-cli": t("userEditSection.presetClients.codex-cli"),

+ 83 - 0
src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx

@@ -415,6 +415,52 @@ export function LogicTraceTab({
                                         )
                                       </span>
                                     )}
+                                    {p.clientRestrictionContext && (
+                                      <div className="ml-4 mt-1 space-y-0.5 text-[10px] text-muted-foreground">
+                                        {p.clientRestrictionContext.matchedPattern && (
+                                          <div>
+                                            {tChain(
+                                              `filterDetails.${p.clientRestrictionContext.matchType}`,
+                                              { pattern: p.clientRestrictionContext.matchedPattern }
+                                            )}
+                                          </div>
+                                        )}
+                                        {!p.clientRestrictionContext.matchedPattern && (
+                                          <div>
+                                            {tChain(
+                                              `filterDetails.${p.clientRestrictionContext.matchType}`
+                                            )}
+                                          </div>
+                                        )}
+                                        {p.clientRestrictionContext.detectedClient && (
+                                          <div>
+                                            {tChain("filterDetails.detectedClient", {
+                                              client: p.clientRestrictionContext.detectedClient,
+                                            })}
+                                          </div>
+                                        )}
+                                        {p.clientRestrictionContext.providerAllowlist.length >
+                                          0 && (
+                                          <div>
+                                            {tChain("filterDetails.providerAllowlist", {
+                                              list: p.clientRestrictionContext.providerAllowlist.join(
+                                                ", "
+                                              ),
+                                            })}
+                                          </div>
+                                        )}
+                                        {p.clientRestrictionContext.providerBlocklist.length >
+                                          0 && (
+                                          <div>
+                                            {tChain("filterDetails.providerBlocklist", {
+                                              list: p.clientRestrictionContext.providerBlocklist.join(
+                                                ", "
+                                              ),
+                                            })}
+                                          </div>
+                                        )}
+                                      </div>
+                                    )}
                                   </div>
                                 ))}
                               </div>
@@ -551,6 +597,43 @@ export function LogicTraceTab({
                           )
                         </span>
                       )}
+                      {p.clientRestrictionContext && (
+                        <div className="ml-4 mt-1 space-y-0.5 text-[10px] text-muted-foreground">
+                          {p.clientRestrictionContext.matchedPattern && (
+                            <div>
+                              {tChain(`filterDetails.${p.clientRestrictionContext.matchType}`, {
+                                pattern: p.clientRestrictionContext.matchedPattern,
+                              })}
+                            </div>
+                          )}
+                          {!p.clientRestrictionContext.matchedPattern && (
+                            <div>
+                              {tChain(`filterDetails.${p.clientRestrictionContext.matchType}`)}
+                            </div>
+                          )}
+                          {p.clientRestrictionContext.detectedClient && (
+                            <div>
+                              {tChain("filterDetails.detectedClient", {
+                                client: p.clientRestrictionContext.detectedClient,
+                              })}
+                            </div>
+                          )}
+                          {p.clientRestrictionContext.providerAllowlist.length > 0 && (
+                            <div>
+                              {tChain("filterDetails.providerAllowlist", {
+                                list: p.clientRestrictionContext.providerAllowlist.join(", "),
+                              })}
+                            </div>
+                          )}
+                          {p.clientRestrictionContext.providerBlocklist.length > 0 && (
+                            <div>
+                              {tChain("filterDetails.providerBlocklist", {
+                                list: p.clientRestrictionContext.providerBlocklist.join(", "),
+                              })}
+                            </div>
+                          )}
+                        </div>
+                      )}
                     </div>
                   ))}
                 </div>

+ 10 - 0
src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx

@@ -6,6 +6,7 @@ import {
   ChevronRight,
   InfoIcon,
   Link2,
+  MinusCircle,
   RefreshCw,
   XCircle,
   Zap,
@@ -33,6 +34,8 @@ interface ProviderChainPopoverProps {
  * Determine if this is an actual request record (excluding intermediate states)
  */
 function isActualRequest(item: ProviderChainItem): boolean {
+  if (item.reason === "client_restriction_filtered") return false;
+
   if (item.reason === "concurrent_limit_failed") return true;
 
   if (item.reason === "retry_failed" || item.reason === "system_error") return true;
@@ -101,6 +104,13 @@ function getItemStatus(item: ProviderChainItem): {
       bgColor: "bg-orange-50 dark:bg-orange-950/30",
     };
   }
+  if (item.reason === "client_restriction_filtered") {
+    return {
+      icon: MinusCircle,
+      color: "text-muted-foreground",
+      bgColor: "bg-muted/30",
+    };
+  }
   return {
     icon: RefreshCw,
     color: "text-slate-500",

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

@@ -318,6 +318,8 @@ function ProviderFormContent({
           model_redirects: state.routing.modelRedirects,
           allowed_models:
             state.routing.allowedModels.length > 0 ? state.routing.allowedModels : null,
+          allowed_clients: state.routing.allowedClients,
+          blocked_clients: state.routing.blockedClients,
           priority: state.routing.priority,
           group_priorities:
             Object.keys(state.routing.groupPriorities).length > 0

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

@@ -28,6 +28,8 @@ const ACTION_TO_FIELD_PATH: Partial<Record<ProviderFormAction["type"], string>>
   SET_PRESERVE_CLIENT_IP: "routing.preserveClientIp",
   SET_MODEL_REDIRECTS: "routing.modelRedirects",
   SET_ALLOWED_MODELS: "routing.allowedModels",
+  SET_ALLOWED_CLIENTS: "routing.allowedClients",
+  SET_BLOCKED_CLIENTS: "routing.blockedClients",
   SET_GROUP_PRIORITIES: "routing.groupPriorities",
   SET_CACHE_TTL_PREFERENCE: "routing.cacheTtlPreference",
   SET_SWAP_CACHE_TTL_BILLING: "routing.swapCacheTtlBilling",
@@ -91,6 +93,8 @@ export function createInitialState(
         preserveClientIp: false,
         modelRedirects: {},
         allowedModels: [],
+        allowedClients: [],
+        blockedClients: [],
         priority: 0,
         groupPriorities: {},
         weight: 1,
@@ -165,6 +169,8 @@ export function createInitialState(
       preserveClientIp: sourceProvider?.preserveClientIp ?? false,
       modelRedirects: sourceProvider?.modelRedirects ?? {},
       allowedModels: sourceProvider?.allowedModels ?? [],
+      allowedClients: sourceProvider?.allowedClients ?? [],
+      blockedClients: sourceProvider?.blockedClients ?? [],
       priority: sourceProvider?.priority ?? 0,
       groupPriorities: sourceProvider?.groupPriorities ?? {},
       weight: sourceProvider?.weight ?? 1,
@@ -262,6 +268,10 @@ export function providerFormReducer(
       return { ...state, routing: { ...state.routing, modelRedirects: action.payload } };
     case "SET_ALLOWED_MODELS":
       return { ...state, routing: { ...state.routing, allowedModels: action.payload } };
+    case "SET_ALLOWED_CLIENTS":
+      return { ...state, routing: { ...state.routing, allowedClients: action.payload } };
+    case "SET_BLOCKED_CLIENTS":
+      return { ...state, routing: { ...state.routing, blockedClients: action.payload } };
     case "SET_PRIORITY":
       return { ...state, routing: { ...state.routing, priority: action.payload } };
     case "SET_GROUP_PRIORITIES":

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

@@ -42,6 +42,8 @@ export interface RoutingState {
   preserveClientIp: boolean;
   modelRedirects: Record<string, string>;
   allowedModels: string[];
+  allowedClients: string[];
+  blockedClients: string[];
   priority: number;
   groupPriorities: Record<string, number>;
   weight: number;
@@ -128,6 +130,8 @@ export type ProviderFormAction =
   | { type: "SET_PRESERVE_CLIENT_IP"; payload: boolean }
   | { type: "SET_MODEL_REDIRECTS"; payload: Record<string, string> }
   | { type: "SET_ALLOWED_MODELS"; payload: string[] }
+  | { type: "SET_ALLOWED_CLIENTS"; payload: string[] }
+  | { type: "SET_BLOCKED_CLIENTS"; payload: string[] }
   | { type: "SET_PRIORITY"; payload: number }
   | { type: "SET_GROUP_PRIORITIES"; payload: Record<string, number> }
   | { type: "SET_WEIGHT"; payload: number }

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

@@ -5,7 +5,9 @@ import { Info, Layers, Route, Scale, Settings, Timer } from "lucide-react";
 import { useTranslations } from "next-intl";
 import { toast } from "sonner";
 import { Badge } from "@/components/ui/badge";
+import { Checkbox } from "@/components/ui/checkbox";
 import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
 import {
   Select,
   SelectContent,
@@ -16,6 +18,14 @@ import {
 import { Switch } from "@/components/ui/switch";
 import { TagInput } from "@/components/ui/tag-input";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import {
+  CLIENT_RESTRICTION_PRESET_OPTIONS,
+  isPresetSelected,
+  mergePresetAndCustomClients,
+  removePresetValues,
+  splitPresetAndCustomClients,
+  togglePresetSelection,
+} from "@/lib/client-restrictions/client-presets";
 import { getProviderTypeConfig } from "@/lib/provider-type-utils";
 import type {
   CodexParallelToolCallsPreference,
@@ -69,6 +79,44 @@ export function RoutingSection() {
   };
 
   const providerTypes: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"];
+  const allowedClients = state.routing.allowedClients;
+  const blockedClients = state.routing.blockedClients;
+  const { customValues: customAllowedClients } = splitPresetAndCustomClients(allowedClients);
+  const { customValues: customBlockedClients } = splitPresetAndCustomClients(blockedClients);
+
+  const handleAllowToggle = (presetValue: string, checked: boolean) => {
+    const nextAllowed = togglePresetSelection(allowedClients, presetValue, checked);
+    dispatch({ type: "SET_ALLOWED_CLIENTS", payload: nextAllowed });
+
+    if (checked) {
+      const nextBlocked = removePresetValues(blockedClients, presetValue);
+      dispatch({ type: "SET_BLOCKED_CLIENTS", payload: nextBlocked });
+    }
+  };
+
+  const handleBlockToggle = (presetValue: string, checked: boolean) => {
+    const nextBlocked = togglePresetSelection(blockedClients, presetValue, checked);
+    dispatch({ type: "SET_BLOCKED_CLIENTS", payload: nextBlocked });
+
+    if (checked) {
+      const nextAllowed = removePresetValues(allowedClients, presetValue);
+      dispatch({ type: "SET_ALLOWED_CLIENTS", payload: nextAllowed });
+    }
+  };
+
+  const handleCustomAllowedChange = (customValues: string[]) => {
+    dispatch({
+      type: "SET_ALLOWED_CLIENTS",
+      payload: mergePresetAndCustomClients(allowedClients, customValues),
+    });
+  };
+
+  const handleCustomBlockedChange = (customValues: string[]) => {
+    dispatch({
+      type: "SET_BLOCKED_CLIENTS",
+      payload: mergePresetAndCustomClients(blockedClients, customValues),
+    });
+  };
 
   return (
     <TooltipProvider>
@@ -219,6 +267,85 @@ export function RoutingSection() {
               </p>
             </FieldGroup>
           </div>
+
+          {/* Client Restrictions */}
+          <FieldGroup label={t("sections.routing.clientRestrictions.allowedLabel")}>
+            <div className="space-y-2 rounded-md border p-3">
+              {CLIENT_RESTRICTION_PRESET_OPTIONS.map((option) => {
+                const isAllowed = isPresetSelected(allowedClients, option.value);
+                const isBlocked = isPresetSelected(blockedClients, option.value);
+                return (
+                  <div key={option.value} className="flex items-center gap-4 py-1">
+                    <span className="flex-1 text-sm">
+                      {t(`sections.routing.clientRestrictions.presetClients.${option.value}`)}
+                    </span>
+                    <div className="flex items-center gap-3">
+                      <div className="flex items-center gap-1.5">
+                        <Checkbox
+                          id={`provider-allow-${option.value}`}
+                          checked={isAllowed}
+                          disabled={state.ui.isPending}
+                          onCheckedChange={(checked) =>
+                            handleAllowToggle(option.value, checked === true)
+                          }
+                        />
+                        <Label
+                          htmlFor={`provider-allow-${option.value}`}
+                          className="cursor-pointer text-xs font-normal text-muted-foreground"
+                        >
+                          {t("sections.routing.clientRestrictions.allowAction")}
+                        </Label>
+                      </div>
+                      <div className="flex items-center gap-1.5">
+                        <Checkbox
+                          id={`provider-block-${option.value}`}
+                          checked={isBlocked}
+                          disabled={state.ui.isPending}
+                          onCheckedChange={(checked) =>
+                            handleBlockToggle(option.value, checked === true)
+                          }
+                        />
+                        <Label
+                          htmlFor={`provider-block-${option.value}`}
+                          className="cursor-pointer text-xs font-normal text-muted-foreground"
+                        >
+                          {t("sections.routing.clientRestrictions.blockAction")}
+                        </Label>
+                      </div>
+                    </div>
+                  </div>
+                );
+              })}
+            </div>
+          </FieldGroup>
+
+          <FieldGroup label={t("sections.routing.clientRestrictions.customAllowedLabel")}>
+            <TagInput
+              value={customAllowedClients}
+              onChange={handleCustomAllowedChange}
+              placeholder={t("sections.routing.clientRestrictions.customAllowedPlaceholder")}
+              maxTagLength={64}
+              maxTags={50}
+              disabled={state.ui.isPending}
+            />
+            <p className="mt-1 text-xs text-muted-foreground">
+              {t("sections.routing.clientRestrictions.customHelp")}
+            </p>
+          </FieldGroup>
+
+          <FieldGroup label={t("sections.routing.clientRestrictions.customBlockedLabel")}>
+            <TagInput
+              value={customBlockedClients}
+              onChange={handleCustomBlockedChange}
+              placeholder={t("sections.routing.clientRestrictions.customBlockedPlaceholder")}
+              maxTagLength={64}
+              maxTags={50}
+              disabled={state.ui.isPending}
+            />
+            <p className="mt-1 text-xs text-muted-foreground">
+              {t("sections.routing.clientRestrictions.customHelp")}
+            </p>
+          </FieldGroup>
         </SectionCard>
 
         {/* Scheduling Parameters */}

+ 236 - 0
src/app/v1/_lib/proxy/client-detector.ts

@@ -0,0 +1,236 @@
+import type { ProxySession } from "./session";
+
+export const CLAUDE_CODE_KEYWORD_PREFIX = "claude-code";
+
+export const BUILTIN_CLIENT_KEYWORDS = new Set([
+  "claude-code",
+  "claude-code-cli",
+  "claude-code-cli-sdk",
+  "claude-code-vscode",
+  "claude-code-sdk-ts",
+  "claude-code-sdk-py",
+  "claude-code-gh-action",
+]);
+
+export interface ClientDetectionResult {
+  matched: boolean;
+  hubConfirmed: boolean;
+  subClient: string | null;
+  signals: string[];
+  supplementary: string[];
+}
+
+export interface ClientRestrictionResult {
+  allowed: boolean;
+  matchType: "no_restriction" | "allowed" | "blocklist_hit" | "allowlist_miss";
+  matchedPattern?: string;
+  detectedClient?: string;
+  checkedAllowlist: string[];
+  checkedBlocklist: string[];
+}
+
+const normalize = (s: string) => s.toLowerCase().replace(/[-_]/g, "");
+
+const ENTRYPOINT_MAP: Record<string, string> = {
+  cli: "claude-code-cli",
+  "sdk-cli": "claude-code-cli-sdk",
+  "claude-vscode": "claude-code-vscode",
+  "sdk-ts": "claude-code-sdk-ts",
+  "sdk-py": "claude-code-sdk-py",
+  "claude-code-github-action": "claude-code-gh-action",
+};
+
+function confirmClaudeCodeSignals(session: ProxySession): {
+  confirmed: boolean;
+  signals: string[];
+  supplementary: string[];
+} {
+  const signals: string[] = [];
+  const supplementary: string[] = [];
+
+  if (session.headers.get("x-app") === "cli") {
+    signals.push("x-app-cli");
+  }
+
+  if (/^claude-cli\//i.test(session.userAgent ?? "")) {
+    signals.push("ua-prefix");
+  }
+
+  const betas = session.request.message["betas"];
+  if (
+    Array.isArray(betas) &&
+    betas.some((beta) => typeof beta === "string" && /^claude-code-/i.test(beta))
+  ) {
+    signals.push("betas-claude-code");
+  }
+
+  if (session.headers.get("anthropic-dangerous-direct-browser-access") === "true") {
+    supplementary.push("dangerous-browser-access");
+  }
+
+  return {
+    confirmed: signals.length === 3,
+    signals,
+    supplementary,
+  };
+}
+
+function extractSubClient(ua: string): string | null {
+  const match = /^claude-cli\/\S+\s+\(external,\s*([^,)]+)/i.exec(ua);
+  if (!match?.[1]) {
+    return null;
+  }
+
+  const entrypoint = match[1].trim();
+  return ENTRYPOINT_MAP[entrypoint] ?? null;
+}
+
+export function isBuiltinKeyword(pattern: string): boolean {
+  return BUILTIN_CLIENT_KEYWORDS.has(pattern);
+}
+
+export function matchClientPattern(session: ProxySession, pattern: string): boolean {
+  if (!isBuiltinKeyword(pattern)) {
+    const ua = session.userAgent?.trim();
+    if (!ua) {
+      return false;
+    }
+
+    const normalizedPattern = normalize(pattern);
+    if (normalizedPattern === "") {
+      return false;
+    }
+
+    return normalize(ua).includes(normalizedPattern);
+  }
+
+  const claudeCode = confirmClaudeCodeSignals(session);
+  if (!claudeCode.confirmed) {
+    return false;
+  }
+
+  if (pattern === CLAUDE_CODE_KEYWORD_PREFIX) {
+    return true;
+  }
+
+  const subClient = extractSubClient(session.userAgent ?? "");
+  return subClient === pattern;
+}
+
+export function detectClientFull(session: ProxySession, pattern: string): ClientDetectionResult {
+  const claudeCode = confirmClaudeCodeSignals(session);
+  const subClient = claudeCode.confirmed ? extractSubClient(session.userAgent ?? "") : null;
+
+  let matched = false;
+  if (isBuiltinKeyword(pattern)) {
+    if (claudeCode.confirmed) {
+      matched =
+        pattern === CLAUDE_CODE_KEYWORD_PREFIX || (subClient !== null && subClient === pattern);
+    }
+  } else {
+    const ua = session.userAgent?.trim();
+    if (ua) {
+      const normalizedPattern = normalize(pattern);
+      if (normalizedPattern !== "") {
+        matched = normalize(ua).includes(normalizedPattern);
+      }
+    }
+  }
+
+  return {
+    matched,
+    hubConfirmed: claudeCode.confirmed,
+    subClient,
+    signals: claudeCode.signals,
+    supplementary: claudeCode.supplementary,
+  };
+}
+
+export function isClientAllowed(
+  session: ProxySession,
+  allowedClients: string[],
+  blockedClients?: string[]
+): boolean {
+  return isClientAllowedDetailed(session, allowedClients, blockedClients).allowed;
+}
+
+export function isClientAllowedDetailed(
+  session: ProxySession,
+  allowedClients: string[],
+  blockedClients?: string[]
+): ClientRestrictionResult {
+  const checkedAllowlist = allowedClients;
+  const checkedBlocklist = blockedClients ?? [];
+
+  const hasBlockList = checkedBlocklist.length > 0;
+  if (!hasBlockList && allowedClients.length === 0) {
+    return {
+      allowed: true,
+      matchType: "no_restriction",
+      checkedAllowlist,
+      checkedBlocklist,
+    };
+  }
+
+  // Pre-compute once to avoid repeated signal checks per pattern
+  const claudeCode = confirmClaudeCodeSignals(session);
+  const ua = session.userAgent?.trim() ?? "";
+  const normalizedUa = normalize(ua);
+  const subClient = claudeCode.confirmed ? extractSubClient(ua) : null;
+  const detectedClient = subClient || ua || undefined;
+
+  const matches = (pattern: string): boolean => {
+    if (!isBuiltinKeyword(pattern)) {
+      if (!ua) return false;
+      const normalizedPattern = normalize(pattern);
+      return normalizedPattern !== "" && normalizedUa.includes(normalizedPattern);
+    }
+    if (!claudeCode.confirmed) return false;
+    if (pattern === CLAUDE_CODE_KEYWORD_PREFIX) return true;
+    return subClient === pattern;
+  };
+
+  if (checkedBlocklist.length > 0) {
+    const blockedPattern = checkedBlocklist.find(matches);
+    if (blockedPattern) {
+      return {
+        allowed: false,
+        matchType: "blocklist_hit",
+        matchedPattern: blockedPattern,
+        detectedClient,
+        checkedAllowlist,
+        checkedBlocklist,
+      };
+    }
+  }
+
+  if (allowedClients.length === 0) {
+    return {
+      allowed: true,
+      matchType: "allowed",
+      detectedClient,
+      checkedAllowlist,
+      checkedBlocklist,
+    };
+  }
+
+  const allowedPattern = allowedClients.find(matches);
+  if (allowedPattern) {
+    return {
+      allowed: true,
+      matchType: "allowed",
+      matchedPattern: allowedPattern,
+      detectedClient,
+      checkedAllowlist,
+      checkedBlocklist,
+    };
+  }
+
+  return {
+    allowed: false,
+    matchType: "allowlist_miss",
+    detectedClient,
+    checkedAllowlist,
+    checkedBlocklist,
+  };
+}

+ 18 - 39
src/app/v1/_lib/proxy/client-guard.ts

@@ -1,21 +1,7 @@
+import { isClientAllowedDetailed } from "./client-detector";
 import { ProxyResponses } from "./responses";
 import type { ProxySession } from "./session";
 
-/**
- * Client (CLI/IDE) restriction guard
- *
- * Validates that the client making the request is allowed based on User-Agent header matching.
- * This check is ONLY performed when the user has configured client restrictions (allowedClients).
- *
- * Logic:
- * - If allowedClients is empty or undefined: skip all checks, allow request
- * - If allowedClients is non-empty:
- *   - Missing or empty User-Agent → 400 error
- *   - User-Agent doesn't match any allowed pattern → 400 error
- *   - User-Agent matches at least one pattern → allow request
- *
- * Matching: case-insensitive substring match
- */
 export class ProxyClientGuard {
   static async ensure(session: ProxySession): Promise<Response | null> {
     const user = session.authState?.user;
@@ -24,18 +10,17 @@ export class ProxyClientGuard {
       return null;
     }
 
-    // Check if client restrictions are configured
     const allowedClients = user.allowedClients ?? [];
-    if (allowedClients.length === 0) {
-      // No restrictions configured - skip all checks
+    const blockedClients = user.blockedClients ?? [];
+
+    if (allowedClients.length === 0 && blockedClients.length === 0) {
       return null;
     }
 
-    // Restrictions exist - now User-Agent is required
-    const userAgent = session.userAgent;
-
-    // Missing or empty User-Agent when restrictions exist
-    if (!userAgent || userAgent.trim() === "") {
+    // User-Agent is only required when an allowlist is configured.
+    // Blocklist-only: no UA can't match any block pattern, so the request passes through.
+    const userAgent = session.userAgent?.trim();
+    if (!userAgent && allowedClients.length > 0) {
       return ProxyResponses.buildError(
         400,
         "Client not allowed. User-Agent header is required when client restrictions are configured.",
@@ -43,23 +28,17 @@ export class ProxyClientGuard {
       );
     }
 
-    // Case-insensitive substring match with hyphen/underscore normalization
-    // This handles variations like "gemini-cli" matching "GeminiCLI" or "gemini_cli"
-    const normalize = (s: string) => s.toLowerCase().replace(/[-_]/g, "");
-    const userAgentNorm = normalize(userAgent);
-    const isAllowed = allowedClients.some((pattern) => {
-      const normalizedPattern = normalize(pattern);
-      // Skip empty patterns to prevent includes("") matching everything
-      if (normalizedPattern === "") return false;
-      return userAgentNorm.includes(normalizedPattern);
-    });
+    const result = isClientAllowedDetailed(session, allowedClients, blockedClients);
 
-    if (!isAllowed) {
-      return ProxyResponses.buildError(
-        400,
-        `Client not allowed. Your client is not in the allowed list.`,
-        "invalid_request_error"
-      );
+    if (!result.allowed) {
+      const detected = result.detectedClient ? ` (detected: ${result.detectedClient})` : "";
+      let message: string;
+      if (result.matchType === "blocklist_hit") {
+        message = `Client blocked by pattern: ${result.matchedPattern}${detected}`;
+      } else {
+        message = `Client not in allowed list: [${allowedClients.join(", ")}]${detected}`;
+      }
+      return ProxyResponses.buildError(400, message, "invalid_request_error");
     }
 
     // Client is allowed

+ 96 - 1
src/app/v1/_lib/proxy/provider-selector.ts

@@ -8,6 +8,7 @@ import { findAllProviders, findProviderById } from "@/repository/provider";
 import { getSystemSettings } from "@/repository/system-config";
 import type { ProviderChainItem } from "@/types/message";
 import type { Provider } from "@/types/provider";
+import { isClientAllowedDetailed } from "./client-detector";
 import type { ClientFormat } from "./format-mapper";
 import { ProxyResponses } from "./responses";
 import type { ProxySession } from "./session";
@@ -423,10 +424,15 @@ export class ProxyProviderResolver {
         const circuitOpen = filteredProviders.filter((p) => p.reason === "circuit_open");
         const disabled = filteredProviders.filter((p) => p.reason === "disabled");
         const modelNotAllowed = filteredProviders.filter((p) => p.reason === "model_not_allowed");
+        const clientRestricted = filteredProviders.filter((p) => p.reason === "client_restriction");
 
         // 计算可用供应商数量(排除禁用和模型不支持的)
         const unavailableCount = rateLimited.length + circuitOpen.length;
-        const totalEnabled = filteredProviders.length - disabled.length - modelNotAllowed.length;
+        const totalEnabled =
+          filteredProviders.length -
+          disabled.length -
+          modelNotAllowed.length -
+          clientRestricted.length;
 
         if (
           rateLimited.length > 0 &&
@@ -473,11 +479,20 @@ export class ProxyProviderResolver {
 
     const filteredProviders = session.getLastSelectionContext()?.filteredProviders;
     if (filteredProviders) {
+      const clientRestricted = filteredProviders.filter((p) => p.reason === "client_restriction");
+
       // C-001: 脱敏供应商名称,仅暴露 id 和 reason
       details.filteredProviders = filteredProviders.map((p) => ({
         id: p.id,
         reason: p.reason,
       }));
+
+      if (clientRestricted.length > 0) {
+        details.clientRestrictedProviders = clientRestricted.map((p) => ({
+          id: p.id,
+          reason: p.reason,
+        }));
+      }
     }
 
     return ProxyResponses.buildError(status, message, errorType, details);
@@ -573,6 +588,55 @@ export class ProxyProviderResolver {
       return null;
     }
 
+    // Check provider-level client restrictions on session reuse
+    const providerAllowed = provider.allowedClients ?? [];
+    const providerBlocked = provider.blockedClients ?? [];
+    const clientResult = isClientAllowedDetailed(session, providerAllowed, providerBlocked);
+    if (!clientResult.allowed) {
+      logger.debug("ProviderSelector: Session provider blocked by client restrictions", {
+        sessionId: session.sessionId,
+        providerId: provider.id,
+        matchType: clientResult.matchType,
+        matchedPattern: clientResult.matchedPattern,
+        detectedClient: clientResult.detectedClient,
+      });
+      session.addProviderToChain(provider, {
+        reason: "client_restriction_filtered",
+        decisionContext: {
+          totalProviders: 0,
+          enabledProviders: 0,
+          targetType: provider.providerType as NonNullable<
+            ProviderChainItem["decisionContext"]
+          >["targetType"],
+          requestedModel: session.getOriginalModel() || "",
+          groupFilterApplied: false,
+          beforeHealthCheck: 0,
+          afterHealthCheck: 0,
+          priorityLevels: [],
+          selectedPriority: 0,
+          candidatesAtPriority: [],
+          filteredProviders: [
+            {
+              id: provider.id,
+              name: provider.name,
+              reason: "client_restriction",
+              details:
+                clientResult.matchType === "blocklist_hit" ? "blocklist_hit" : "allowlist_miss",
+              clientRestrictionContext: {
+                matchType: clientResult.matchType as "blocklist_hit" | "allowlist_miss",
+                matchedPattern: clientResult.matchedPattern,
+                detectedClient: clientResult.detectedClient,
+                providerAllowlist: clientResult.checkedAllowlist,
+                providerBlocklist: clientResult.checkedBlocklist,
+              },
+            },
+          ],
+        },
+      });
+      await SessionManager.clearSessionProvider(session.sessionId);
+      return null;
+    }
+
     // 修复:检查用户分组权限(严格分组隔离 + 支持多分组)
     // Check if session provider matches user's group
     // Priority: key.providerGroup > user.providerGroup
@@ -749,6 +813,37 @@ export class ProxyProviderResolver {
       excludedProviderIds: excludeIds.length > 0 ? excludeIds : undefined,
     };
 
+    if (session) {
+      const clientFilteredProviders: typeof visibleProviders = [];
+      for (const p of visibleProviders) {
+        const providerAllowed = p.allowedClients ?? [];
+        const providerBlocked = p.blockedClients ?? [];
+        if (providerAllowed.length === 0 && providerBlocked.length === 0) {
+          clientFilteredProviders.push(p);
+          continue;
+        }
+        const result = isClientAllowedDetailed(session, providerAllowed, providerBlocked);
+        if (!result.allowed) {
+          context.filteredProviders?.push({
+            id: p.id,
+            name: p.name,
+            reason: "client_restriction",
+            details: result.matchType === "blocklist_hit" ? "blocklist_hit" : "allowlist_miss",
+            clientRestrictionContext: {
+              matchType: result.matchType as "blocklist_hit" | "allowlist_miss",
+              matchedPattern: result.matchedPattern,
+              detectedClient: result.detectedClient,
+              providerAllowlist: result.checkedAllowlist,
+              providerBlocklist: result.checkedBlocklist,
+            },
+          });
+          continue;
+        }
+        clientFilteredProviders.push(p);
+      }
+      visibleProviders = clientFilteredProviders;
+    }
+
     // Step 2: 基础过滤 + 格式/模型匹配(使用 visibleProviders)
     const enabledProviders = visibleProviders.filter((provider) => {
       // 2a. 基础过滤

+ 2 - 1
src/app/v1/_lib/proxy/session.ts

@@ -449,7 +449,8 @@ export class ProxySession {
         | "client_error_non_retryable" // 不可重试的客户端错误(Prompt 超限、内容过滤、PDF 限制、Thinking 格式)
         | "http2_fallback" // HTTP/2 协议错误,回退到 HTTP/1.1(不切换供应商、不计入熔断器)
         | "endpoint_pool_exhausted" // 端点池耗尽(strict endpoint policy 阻止了 fallback)
-        | "vendor_type_all_timeout"; // 供应商类型全端点超时(524),触发 vendor-type 临时熔断
+        | "vendor_type_all_timeout" // 供应商类型全端点超时(524),触发 vendor-type 临时熔断
+        | "client_restriction_filtered"; // 供应商因客户端限制被跳过(会话复用路径)
       selectionMethod?:
         | "session_reuse"
         | "weighted_random"

+ 28 - 1
src/drizzle/schema.ts

@@ -72,6 +72,10 @@ export const users = pgTable('users', {
   // Empty array = no restrictions, non-empty = only listed models allowed
   allowedModels: jsonb('allowed_models').$type<string[]>().default([]),
 
+  // Blocked clients (CLI/IDE blocklist)
+  // Non-empty = listed patterns are denied even if allowedClients permits them
+  blockedClients: jsonb('blocked_clients').$type<string[]>().notNull().default([]),
+
   createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
   updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
   deletedAt: timestamp('deleted_at', { withTimezone: true }),
@@ -193,6 +197,12 @@ export const providers = pgTable('providers', {
   // - null 或空数组:Anthropic 允许所有 claude 模型,非 Anthropic 允许任意模型
   allowedModels: jsonb('allowed_models').$type<string[] | null>().default(null),
 
+  // Client restrictions for this provider
+  // allowedClients: empty = no restriction; non-empty = only listed patterns allowed
+  // blockedClients: non-empty = listed patterns are denied
+  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),
@@ -542,6 +552,13 @@ export const messageRequest = pgTable('message_request', {
   messageRequestKeyCostActiveIdx: index('idx_message_request_key_cost_active')
     .on(table.key, table.costUsd)
     .where(sql`${table.deletedAt} IS NULL AND (${table.blockedBy} IS NULL OR ${table.blockedBy} <> 'warmup')`),
+  // #slow-query: composite index for session user-info LATERAL lookup
+  // Query: WHERE session_id = $1 AND deleted_at IS NULL ORDER BY created_at LIMIT 1
+  // Provides seek + pre-sorted scan; user_id, key in index reduce heap columns to fetch.
+  // user_agent/api_type still require one heap fetch per session (LIMIT 1, negligible).
+  messageRequestSessionUserInfoIdx: index('idx_message_request_session_user_info')
+    .on(table.sessionId, table.createdAt, table.userId, table.key)
+    .where(sql`${table.deletedAt} IS NULL`),
 }));
 
 // Model Prices table
@@ -881,8 +898,18 @@ export const usageLedger = pgTable('usage_ledger', {
   usageLedgerModelIdx: index('idx_usage_ledger_model')
     .on(table.model)
     .where(sql`${table.model} IS NOT NULL`),
+  // #slow-query: covering index for SUM(cost_usd) per key (replaces old key+cost, adds created_at for time range)
   usageLedgerKeyCostIdx: index('idx_usage_ledger_key_cost')
-    .on(table.key, table.costUsd)
+    .on(table.key, table.createdAt, table.costUsd)
+    .where(sql`${table.blockedBy} IS NULL`),
+  // #slow-query: covering index for SUM(cost_usd) per user (Quotas page + rate-limit total)
+  // Keys: user_id (equality), created_at (range filter), cost_usd (aggregation, index-only scan)
+  usageLedgerUserCostCoverIdx: index('idx_usage_ledger_user_cost_cover')
+    .on(table.userId, table.createdAt, table.costUsd)
+    .where(sql`${table.blockedBy} IS NULL`),
+  // #slow-query: covering index for SUM(cost_usd) per provider (rate-limit total)
+  usageLedgerProviderCostCoverIdx: index('idx_usage_ledger_provider_cost_cover')
+    .on(table.finalProviderId, table.createdAt, table.costUsd)
     .where(sql`${table.blockedBy} IS NULL`),
 }));
 

+ 50 - 0
src/lib/client-restrictions/client-presets.test.ts

@@ -0,0 +1,50 @@
+import { describe, expect, test } from "vitest";
+import {
+  isPresetClientValue,
+  isPresetSelected,
+  mergePresetAndCustomClients,
+  removePresetValues,
+  splitPresetAndCustomClients,
+  togglePresetSelection,
+} from "./client-presets";
+
+describe("client restriction presets", () => {
+  test("treats Claude Code sub-client values as preset-compatible aliases", () => {
+    expect(isPresetClientValue("claude-code")).toBe(true);
+    expect(isPresetClientValue("claude-code-cli")).toBe(true);
+    expect(isPresetSelected(["claude-code-cli-sdk"], "claude-code")).toBe(true);
+  });
+
+  test("splitPresetAndCustomClients keeps legacy aliases in presetValues", () => {
+    const result = splitPresetAndCustomClients(["claude-code-vscode", "my-ide"]);
+    expect(result).toEqual({
+      presetValues: ["claude-code-vscode"],
+      customValues: ["my-ide"],
+    });
+  });
+
+  test("togglePresetSelection adds canonical value for newly enabled preset", () => {
+    expect(togglePresetSelection(["gemini-cli"], "claude-code", true)).toEqual([
+      "gemini-cli",
+      "claude-code",
+    ]);
+  });
+
+  test("togglePresetSelection removes canonical value and aliases when disabled", () => {
+    expect(
+      togglePresetSelection(["claude-code", "claude-code-cli", "my-ide"], "claude-code", false)
+    ).toEqual(["my-ide"]);
+  });
+
+  test("removePresetValues clears the whole preset group", () => {
+    expect(removePresetValues(["claude-code-gh-action", "codex-cli"], "claude-code")).toEqual([
+      "codex-cli",
+    ]);
+  });
+
+  test("mergePresetAndCustomClients preserves legacy preset values without forcing migration", () => {
+    expect(
+      mergePresetAndCustomClients(["claude-code-sdk-ts", "codex-cli"], ["my-ide", "codex-cli"])
+    ).toEqual(["claude-code-sdk-ts", "codex-cli", "my-ide"]);
+  });
+});

+ 89 - 0
src/lib/client-restrictions/client-presets.ts

@@ -0,0 +1,89 @@
+export interface ClientRestrictionPresetOption {
+  value: string;
+  aliases: readonly string[];
+}
+
+const CLAUDE_CODE_ALIAS_VALUES = [
+  "claude-code",
+  "claude-code-cli",
+  "claude-code-cli-sdk",
+  "claude-code-vscode",
+  "claude-code-sdk-ts",
+  "claude-code-sdk-py",
+  "claude-code-gh-action",
+] as const;
+
+export const CLIENT_RESTRICTION_PRESET_OPTIONS: readonly ClientRestrictionPresetOption[] = [
+  { value: "claude-code", aliases: CLAUDE_CODE_ALIAS_VALUES },
+  { value: "gemini-cli", aliases: ["gemini-cli"] },
+  { value: "factory-cli", aliases: ["factory-cli"] },
+  { value: "codex-cli", aliases: ["codex-cli"] },
+];
+
+const PRESET_OPTION_MAP = new Map(
+  CLIENT_RESTRICTION_PRESET_OPTIONS.map((option) => [option.value, option] as const)
+);
+
+const PRESET_ALIAS_SET = new Set(
+  CLIENT_RESTRICTION_PRESET_OPTIONS.flatMap((option) => [...option.aliases])
+);
+
+function uniqueOrdered(values: string[]): string[] {
+  const seen = new Set<string>();
+  const result: string[] = [];
+  for (const value of values) {
+    if (seen.has(value)) continue;
+    seen.add(value);
+    result.push(value);
+  }
+  return result;
+}
+
+function getPresetAliases(presetValue: string): readonly string[] {
+  return PRESET_OPTION_MAP.get(presetValue)?.aliases ?? [presetValue];
+}
+
+export function isPresetClientValue(value: string): boolean {
+  return PRESET_ALIAS_SET.has(value);
+}
+
+export function isPresetSelected(values: string[], presetValue: string): boolean {
+  const aliases = getPresetAliases(presetValue);
+  return values.some((value) => aliases.includes(value));
+}
+
+export function removePresetValues(values: string[], presetValue: string): string[] {
+  const aliases = new Set(getPresetAliases(presetValue));
+  return values.filter((value) => !aliases.has(value));
+}
+
+export function togglePresetSelection(
+  values: string[],
+  presetValue: string,
+  checked: boolean
+): string[] {
+  if (!checked) {
+    return removePresetValues(values, presetValue);
+  }
+
+  if (isPresetSelected(values, presetValue)) {
+    return uniqueOrdered(values);
+  }
+
+  return uniqueOrdered([...values, presetValue]);
+}
+
+export function splitPresetAndCustomClients(values: string[]): {
+  presetValues: string[];
+  customValues: string[];
+} {
+  const presetValues = values.filter((value) => PRESET_ALIAS_SET.has(value));
+  const customValues = values.filter((value) => !PRESET_ALIAS_SET.has(value));
+  return { presetValues, customValues };
+}
+
+export function mergePresetAndCustomClients(values: string[], customValues: string[]): string[] {
+  const { presetValues } = splitPresetAndCustomClients(values);
+  const filteredCustomValues = customValues.filter((value) => !PRESET_ALIAS_SET.has(value));
+  return uniqueOrdered([...presetValues, ...filteredCustomValues]);
+}

+ 1 - 1
src/lib/database-backup/docker-executor.ts

@@ -1,7 +1,7 @@
 import { spawn } from "node:child_process";
 import { createReadStream } from "node:fs";
-import { db } from "@/drizzle/db";
 import { sql } from "drizzle-orm";
+import { db } from "@/drizzle/db";
 import { logger } from "@/lib/logger";
 import { getDatabaseConfig } from "./db-config";
 

+ 1 - 0
src/lib/permissions/user-field-permissions.ts

@@ -28,6 +28,7 @@ export const USER_FIELD_PERMISSIONS = {
 
   // Admin-only field (client restrictions)
   allowedClients: { requiredRole: "admin" },
+  blockedClients: { requiredRole: "admin" },
 
   // Admin-only field (model restrictions)
   allowedModels: { requiredRole: "admin" },

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

@@ -31,6 +31,8 @@ const PATCH_FIELDS: ProviderBatchPatchField[] = [
   "group_tag",
   "model_redirects",
   "allowed_models",
+  "allowed_clients",
+  "blocked_clients",
   "anthropic_thinking_budget_preference",
   "anthropic_adaptive_thinking",
   // Routing
@@ -79,6 +81,8 @@ const CLEARABLE_FIELDS: Record<ProviderBatchPatchField, boolean> = {
   group_tag: true,
   model_redirects: true,
   allowed_models: true,
+  allowed_clients: true,
+  blocked_clients: true,
   anthropic_thinking_budget_preference: true,
   anthropic_adaptive_thinking: true,
   // Routing
@@ -395,6 +399,12 @@ export function normalizeProviderBatchPatchDraft(
   const allowedModels = normalizePatchField("allowed_models", typedDraft.allowed_models);
   if (!allowedModels.ok) return allowedModels;
 
+  const allowedClients = normalizePatchField("allowed_clients", typedDraft.allowed_clients);
+  if (!allowedClients.ok) return allowedClients;
+
+  const blockedClients = normalizePatchField("blocked_clients", typedDraft.blocked_clients);
+  if (!blockedClients.ok) return blockedClients;
+
   const thinkingBudget = normalizePatchField(
     "anthropic_thinking_budget_preference",
     typedDraft.anthropic_thinking_budget_preference
@@ -566,6 +576,8 @@ export function normalizeProviderBatchPatchDraft(
       group_tag: groupTag.data,
       model_redirects: modelRedirects.data,
       allowed_models: allowedModels.data,
+      allowed_clients: allowedClients.data,
+      blocked_clients: blockedClients.data,
       anthropic_thinking_budget_preference: thinkingBudget.data,
       anthropic_adaptive_thinking: adaptiveThinking.data,
       // Routing
@@ -642,6 +654,12 @@ function applyPatchField<T>(
             ? (patch.value as ProviderBatchApplyUpdates["allowed_models"])
             : null;
         return { ok: true, data: undefined };
+      case "allowed_clients":
+        updates.allowed_clients = patch.value as ProviderBatchApplyUpdates["allowed_clients"];
+        return { ok: true, data: undefined };
+      case "blocked_clients":
+        updates.blocked_clients = patch.value as ProviderBatchApplyUpdates["blocked_clients"];
+        return { ok: true, data: undefined };
       case "anthropic_thinking_budget_preference":
         updates.anthropic_thinking_budget_preference =
           patch.value as ProviderBatchApplyUpdates["anthropic_thinking_budget_preference"];
@@ -780,6 +798,12 @@ function applyPatchField<T>(
     case "allowed_models":
       updates.allowed_models = null;
       return { ok: true, data: undefined };
+    case "allowed_clients":
+      updates.allowed_clients = [];
+      return { ok: true, data: undefined };
+    case "blocked_clients":
+      updates.blocked_clients = [];
+      return { ok: true, data: undefined };
     case "anthropic_thinking_budget_preference":
       updates.anthropic_thinking_budget_preference = "inherit";
       return { ok: true, data: undefined };
@@ -861,6 +885,8 @@ export function buildProviderBatchApplyUpdates(
     ["group_tag", patch.group_tag],
     ["model_redirects", patch.model_redirects],
     ["allowed_models", patch.allowed_models],
+    ["allowed_clients", patch.allowed_clients],
+    ["blocked_clients", patch.blocked_clients],
     ["anthropic_thinking_budget_preference", patch.anthropic_thinking_budget_preference],
     ["anthropic_adaptive_thinking", patch.anthropic_adaptive_thinking],
     // Routing
@@ -922,6 +948,8 @@ export function hasProviderBatchPatchChanges(patch: ProviderBatchPatch): boolean
     patch.group_tag.mode !== "no_change" ||
     patch.model_redirects.mode !== "no_change" ||
     patch.allowed_models.mode !== "no_change" ||
+    patch.allowed_clients.mode !== "no_change" ||
+    patch.blocked_clients.mode !== "no_change" ||
     patch.anthropic_thinking_budget_preference.mode !== "no_change" ||
     patch.anthropic_adaptive_thinking.mode !== "no_change" ||
     // Routing

+ 37 - 1
src/lib/utils/provider-chain-formatter.ts

@@ -400,6 +400,27 @@ export function formatProviderTimeline(
       continue;
     }
 
+    // === Session reuse client restriction ===
+    if (item.reason === "client_restriction_filtered" && ctx) {
+      timeline += `${t("filterDetails.session_reuse_client_restriction")}\n\n`;
+      timeline += `${t("timeline.provider", { provider: item.name })}\n`;
+      if (ctx.filteredProviders && ctx.filteredProviders.length > 0) {
+        const f = ctx.filteredProviders[0];
+        if (f.clientRestrictionContext) {
+          const crc = f.clientRestrictionContext;
+          const detailKey = `filterDetails.${crc.matchType}`;
+          const detailsText = crc.matchedPattern
+            ? t(detailKey, { pattern: crc.matchedPattern })
+            : t(detailKey);
+          timeline += `${detailsText}\n`;
+          if (crc.detectedClient) {
+            timeline += `${t("filterDetails.detectedClient", { client: crc.detectedClient })}\n`;
+          }
+        }
+      }
+      continue;
+    }
+
     // === 首次选择 ===
     if (item.reason === "initial_selection" && ctx) {
       timeline += `${t("timeline.initialSelectionTitle")}\n\n`;
@@ -425,13 +446,28 @@ export function formatProviderTimeline(
       if (ctx.filteredProviders && ctx.filteredProviders.length > 0) {
         timeline += `\n${t("timeline.filtered")}:\n`;
         for (const f of ctx.filteredProviders) {
-          const icon = f.reason === "circuit_open" ? "⚡" : "💰";
+          const icon =
+            f.reason === "circuit_open" ? "⚡" : f.reason === "client_restriction" ? "🚫" : "💰";
           const detailsText = f.details
             ? t(`filterDetails.${f.details}`) !== `filterDetails.${f.details}`
               ? t(`filterDetails.${f.details}`)
               : f.details
             : f.reason;
           timeline += `  ${icon} ${f.name} (${detailsText})\n`;
+
+          // Client restriction context details
+          if (f.clientRestrictionContext) {
+            const crc = f.clientRestrictionContext;
+            if (crc.detectedClient) {
+              timeline += `    ${t("filterDetails.detectedClient", { client: crc.detectedClient })}\n`;
+            }
+            if (crc.providerAllowlist.length > 0) {
+              timeline += `    ${t("filterDetails.providerAllowlist", { list: crc.providerAllowlist.join(", ") })}\n`;
+            }
+            if (crc.providerBlocklist.length > 0) {
+              timeline += `    ${t("filterDetails.providerBlocklist", { list: crc.providerBlocklist.join(", ") })}\n`;
+            }
+          }
         }
       }
 

+ 56 - 1
src/lib/validation/schemas.test.ts

@@ -1,6 +1,11 @@
 import { describe, expect, test } from "vitest";
 
-import { CreateProviderSchema, UpdateProviderSchema } from "./schemas";
+import {
+  CreateProviderSchema,
+  CreateUserSchema,
+  UpdateProviderSchema,
+  UpdateUserSchema,
+} from "./schemas";
 
 describe("Provider schemas - priority/weight/costMultiplier 规则对齐", () => {
   describe("UpdateProviderSchema", () => {
@@ -99,5 +104,55 @@ describe("Provider schemas - priority/weight/costMultiplier 规则对齐", () =>
       );
       // 注意: null 会被 coerce 转为 0 (Number(null) === 0),所以会通过
     });
+
+    test("allowed_clients/blocked_clients 支持 null 并归一化为空数组", () => {
+      const base = {
+        name: "测试供应商",
+        url: "https://api.example.com",
+        key: "sk-test",
+      };
+
+      const parsed = CreateProviderSchema.parse({
+        ...base,
+        allowed_clients: null,
+        blocked_clients: null,
+      });
+
+      expect(parsed.allowed_clients).toEqual([]);
+      expect(parsed.blocked_clients).toEqual([]);
+    });
+  });
+
+  describe("client restrictions null normalization", () => {
+    test("UpdateProviderSchema 将 null 归一化为空数组", () => {
+      const parsed = UpdateProviderSchema.parse({
+        allowed_clients: null,
+        blocked_clients: null,
+      });
+
+      expect(parsed.allowed_clients).toEqual([]);
+      expect(parsed.blocked_clients).toEqual([]);
+    });
+
+    test("CreateUserSchema 将 null 归一化为空数组", () => {
+      const parsed = CreateUserSchema.parse({
+        name: "test-user",
+        allowedClients: null,
+        blockedClients: null,
+      });
+
+      expect(parsed.allowedClients).toEqual([]);
+      expect(parsed.blockedClients).toEqual([]);
+    });
+
+    test("UpdateUserSchema 将 null 归一化为空数组", () => {
+      const parsed = UpdateUserSchema.parse({
+        allowedClients: null,
+        blockedClients: null,
+      });
+
+      expect(parsed.allowedClients).toEqual([]);
+      expect(parsed.blockedClients).toEqual([]);
+    });
   });
 });

+ 25 - 9
src/lib/validation/schemas.ts

@@ -74,6 +74,21 @@ const ANTHROPIC_ADAPTIVE_THINKING_CONFIG = z
 // - 'disabled': force remove googleSearch tool from request
 const GEMINI_GOOGLE_SEARCH_PREFERENCE = z.enum(["inherit", "enabled", "disabled"]);
 
+const CLIENT_PATTERN_SCHEMA = z
+  .string()
+  .trim()
+  .min(1, "客户端模式不能为空")
+  .max(64, "客户端模式长度不能超过64个字符");
+const CLIENT_PATTERN_ARRAY_SCHEMA = z
+  .array(CLIENT_PATTERN_SCHEMA)
+  .max(50, "客户端模式数量不能超过50个");
+const OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA = z.preprocess(
+  (value) => (value === null ? [] : value),
+  CLIENT_PATTERN_ARRAY_SCHEMA.optional()
+);
+const OPTIONAL_CLIENT_PATTERN_ARRAY_WITH_DEFAULT_SCHEMA =
+  OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA.default([]);
+
 /**
  * 用户创建数据验证schema
  */
@@ -197,11 +212,9 @@ export const CreateUserSchema = z.object({
     .optional()
     .default("00:00"),
   // Allowed clients (CLI/IDE restrictions)
-  allowedClients: z
-    .array(z.string().max(64, "客户端模式长度不能超过64个字符"))
-    .max(50, "客户端模式数量不能超过50个")
-    .optional()
-    .default([]),
+  allowedClients: OPTIONAL_CLIENT_PATTERN_ARRAY_WITH_DEFAULT_SCHEMA,
+  // Blocked clients (CLI/IDE restrictions)
+  blockedClients: OPTIONAL_CLIENT_PATTERN_ARRAY_WITH_DEFAULT_SCHEMA,
   // Allowed models (AI model restrictions)
   allowedModels: z
     .array(z.string().max(64, "模型名称长度不能超过64个字符"))
@@ -322,10 +335,9 @@ export const UpdateUserSchema = z.object({
     .regex(/^([01]\d|2[0-3]):[0-5]\d$/, "重置时间格式必须为 HH:mm")
     .optional(),
   // Allowed clients (CLI/IDE restrictions)
-  allowedClients: z
-    .array(z.string().max(64, "客户端模式长度不能超过64个字符"))
-    .max(50, "客户端模式数量不能超过50个")
-    .optional(),
+  allowedClients: OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA,
+  // Blocked clients (CLI/IDE restrictions)
+  blockedClients: OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA,
   // Allowed models (AI model restrictions)
   allowedModels: z
     .array(z.string().max(64, "模型名称长度不能超过64个字符"))
@@ -437,6 +449,8 @@ export const CreateProviderSchema = z
     preserve_client_ip: z.boolean().optional().default(false),
     model_redirects: z.record(z.string(), z.string()).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,
     // MCP 透传配置
     mcp_passthrough_type: z.enum(["none", "minimax", "glm", "custom"]).optional().default("none"),
     mcp_passthrough_url: z
@@ -643,6 +657,8 @@ export const UpdateProviderSchema = z
     preserve_client_ip: z.boolean().optional(),
     model_redirects: z.record(z.string(), z.string()).nullable().optional(),
     allowed_models: z.array(z.string()).nullable().optional(),
+    allowed_clients: OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA,
+    blocked_clients: OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA,
     // MCP 透传配置
     mcp_passthrough_type: z.enum(["none", "minimax", "glm", "custom"]).optional(),
     mcp_passthrough_url: z

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

@@ -48,6 +48,7 @@ export function toUser(dbUser: any): User {
     isEnabled: dbUser?.isEnabled ?? true,
     expiresAt: dbUser?.expiresAt ? new Date(dbUser.expiresAt) : null,
     allowedClients: dbUser?.allowedClients ?? [],
+    blockedClients: dbUser?.blockedClients ?? [],
     allowedModels: dbUser?.allowedModels ?? [],
     createdAt: dbUser?.createdAt ? new Date(dbUser.createdAt) : new Date(),
     updatedAt: dbUser?.updatedAt ? new Date(dbUser.updatedAt) : new Date(),

+ 56 - 26
src/repository/message.ts

@@ -837,33 +837,63 @@ export async function aggregateMultipleSessionStats(sessionIds: string[]): Promi
   }
 
   // 4. 批量获取用户信息(每个 session 的第一条请求)
-  // 使用 DISTINCT ON + ORDER BY 优化
-  const userInfoResults = await db
-    .select({
-      sessionId: messageRequest.sessionId,
-      userName: users.name,
-      userId: users.id,
-      keyName: keysTable.name,
-      keyId: keysTable.id,
-      userAgent: messageRequest.userAgent,
-      apiType: messageRequest.apiType,
-      createdAt: messageRequest.createdAt,
-    })
-    .from(messageRequest)
-    .innerJoin(users, eq(messageRequest.userId, users.id))
-    .innerJoin(keysTable, eq(messageRequest.key, keysTable.key))
-    .where(and(inArray(messageRequest.sessionId, sessionIds), isNull(messageRequest.deletedAt)))
-    .orderBy(messageRequest.sessionId, messageRequest.createdAt);
-
-  // 创建 sessionId → userInfo 的 Map(取每个 session 最早的记录)
-  const userInfoMap = new Map<string, (typeof userInfoResults)[0]>();
-  for (const info of userInfoResults) {
-    // 跳过 null sessionId(虽然 WHERE 条件已过滤,但需要满足 TypeScript 类型检查)
-    if (!info.sessionId) continue;
-
-    if (!userInfoMap.has(info.sessionId)) {
-      userInfoMap.set(info.sessionId, info);
+  // LATERAL JOIN: 每个 session_id 做 1 次索引探测,无全局排序
+  const sessionIdParams = sql.join(
+    sessionIds.map((id) => sql`${id}`),
+    sql.raw(", ")
+  );
+  const userInfoRows = await db.execute(sql`
+    SELECT
+      sid AS session_id,
+      u.name AS user_name,
+      u.id AS user_id,
+      k.name AS key_name,
+      k.id AS key_id,
+      mr.user_agent,
+      mr.api_type
+    FROM unnest(ARRAY[${sessionIdParams}]::varchar[]) AS sid
+    CROSS JOIN LATERAL (
+      SELECT user_id, key, user_agent, api_type
+      FROM message_request
+      WHERE session_id = sid AND deleted_at IS NULL
+      ORDER BY created_at
+      LIMIT 1
+    ) mr
+    INNER JOIN users u ON mr.user_id = u.id
+    INNER JOIN keys k ON mr.key = k.key
+  `);
+
+  // 创建 sessionId → userInfo 的 Map
+  const userInfoMap = new Map<
+    string,
+    {
+      sessionId: string;
+      userName: string;
+      userId: number;
+      keyName: string;
+      keyId: number;
+      userAgent: string | null;
+      apiType: string | null;
     }
+  >();
+  for (const row of Array.from(userInfoRows) as Array<{
+    session_id: string;
+    user_name: string;
+    user_id: number;
+    key_name: string;
+    key_id: number;
+    user_agent: string | null;
+    api_type: string | null;
+  }>) {
+    userInfoMap.set(row.session_id, {
+      sessionId: row.session_id,
+      userName: row.user_name,
+      userId: row.user_id,
+      keyName: row.key_name,
+      keyId: row.key_id,
+      userAgent: row.user_agent,
+      apiType: row.api_type,
+    });
   }
 
   // 5. 组装最终结果

+ 24 - 0
src/repository/provider.ts

@@ -181,6 +181,8 @@ export async function createProvider(providerData: CreateProviderData): Promise<
     preserveClientIp: providerData.preserve_client_ip ?? false,
     modelRedirects: providerData.model_redirects,
     allowedModels: providerData.allowed_models,
+    allowedClients: providerData.allowed_clients ?? [],
+    blockedClients: providerData.blocked_clients ?? [],
     mcpPassthroughType: providerData.mcp_passthrough_type ?? "none",
     mcpPassthroughUrl: providerData.mcp_passthrough_url ?? null,
     limit5hUsd: providerData.limit_5h_usd != null ? providerData.limit_5h_usd.toString() : null,
@@ -256,6 +258,8 @@ export async function createProvider(providerData: CreateProviderData): Promise<
         preserveClientIp: providers.preserveClientIp,
         modelRedirects: providers.modelRedirects,
         allowedModels: providers.allowedModels,
+        allowedClients: providers.allowedClients,
+        blockedClients: providers.blockedClients,
         mcpPassthroughType: providers.mcpPassthroughType,
         mcpPassthroughUrl: providers.mcpPassthroughUrl,
         limit5hUsd: providers.limit5hUsd,
@@ -336,6 +340,8 @@ export async function findProviderList(
       preserveClientIp: providers.preserveClientIp,
       modelRedirects: providers.modelRedirects,
       allowedModels: providers.allowedModels,
+      allowedClients: providers.allowedClients,
+      blockedClients: providers.blockedClients,
       mcpPassthroughType: providers.mcpPassthroughType,
       mcpPassthroughUrl: providers.mcpPassthroughUrl,
       limit5hUsd: providers.limit5hUsd,
@@ -416,6 +422,8 @@ export async function findAllProvidersFresh(): Promise<Provider[]> {
       preserveClientIp: providers.preserveClientIp,
       modelRedirects: providers.modelRedirects,
       allowedModels: providers.allowedModels,
+      allowedClients: providers.allowedClients,
+      blockedClients: providers.blockedClients,
       mcpPassthroughType: providers.mcpPassthroughType,
       mcpPassthroughUrl: providers.mcpPassthroughUrl,
       limit5hUsd: providers.limit5hUsd,
@@ -500,6 +508,8 @@ export async function findProviderById(id: number): Promise<Provider | null> {
       preserveClientIp: providers.preserveClientIp,
       modelRedirects: providers.modelRedirects,
       allowedModels: providers.allowedModels,
+      allowedClients: providers.allowedClients,
+      blockedClients: providers.blockedClients,
       mcpPassthroughType: providers.mcpPassthroughType,
       mcpPassthroughUrl: providers.mcpPassthroughUrl,
       limit5hUsd: providers.limit5hUsd,
@@ -578,6 +588,10 @@ export async function updateProvider(
   if (providerData.model_redirects !== undefined)
     dbData.modelRedirects = providerData.model_redirects;
   if (providerData.allowed_models !== undefined) dbData.allowedModels = providerData.allowed_models;
+  if (providerData.allowed_clients !== undefined)
+    dbData.allowedClients = providerData.allowed_clients ?? [];
+  if (providerData.blocked_clients !== undefined)
+    dbData.blockedClients = providerData.blocked_clients ?? [];
   if (providerData.mcp_passthrough_type !== undefined)
     dbData.mcpPassthroughType = providerData.mcp_passthrough_type;
   if (providerData.mcp_passthrough_url !== undefined)
@@ -723,6 +737,8 @@ export async function updateProvider(
         preserveClientIp: providers.preserveClientIp,
         modelRedirects: providers.modelRedirects,
         allowedModels: providers.allowedModels,
+        allowedClients: providers.allowedClients,
+        blockedClients: providers.blockedClients,
         mcpPassthroughType: providers.mcpPassthroughType,
         mcpPassthroughUrl: providers.mcpPassthroughUrl,
         limit5hUsd: providers.limit5hUsd,
@@ -973,6 +989,8 @@ export interface BatchProviderUpdates {
   groupTag?: string | null;
   modelRedirects?: Record<string, string> | null;
   allowedModels?: string[] | null;
+  allowedClients?: string[] | null;
+  blockedClients?: string[] | null;
   anthropicThinkingBudgetPreference?: string | null;
   anthropicAdaptiveThinking?: AnthropicAdaptiveThinkingConfig | null;
   // Routing
@@ -1045,6 +1063,12 @@ export async function updateProvidersBatch(
   if (updates.allowedModels !== undefined) {
     setClauses.allowedModels = updates.allowedModels;
   }
+  if (updates.allowedClients !== undefined) {
+    setClauses.allowedClients = updates.allowedClients;
+  }
+  if (updates.blockedClients !== undefined) {
+    setClauses.blockedClients = updates.blockedClients;
+  }
   if (updates.anthropicThinkingBudgetPreference !== undefined) {
     setClauses.anthropicThinkingBudgetPreference = updates.anthropicThinkingBudgetPreference;
   }

+ 48 - 20
src/repository/statistics.ts

@@ -1,5 +1,6 @@
 import "server-only";
 
+import type { SQL } from "drizzle-orm";
 import { and, eq, gte, inArray, isNull, lt, sql } from "drizzle-orm";
 import { db } from "@/drizzle/db";
 import { keys, messageRequest, usageLedger } from "@/drizzle/schema";
@@ -502,56 +503,83 @@ export async function sumUserTotalCost(userId: number, maxAgeDays: number = 365)
 }
 
 /**
- * Batch query: all-time total cost grouped by user_id (single SQL query)
+ * Batch query: total cost grouped by user_id (single SQL query)
  * @param userIds - Array of user IDs
+ * @param maxAgeDays - Only include records newer than this many days (default 365, use Infinity to include all)
  * @returns Map of userId -> totalCost
  */
-export async function sumUserTotalCostBatch(userIds: number[]): Promise<Map<number, number>> {
+export async function sumUserTotalCostBatch(
+  userIds: number[],
+  maxAgeDays: number = 365
+): Promise<Map<number, number>> {
   const result = new Map<number, number>();
   if (userIds.length === 0) return result;
 
+  const conditions: SQL[] = [inArray(usageLedger.userId, userIds), LEDGER_BILLING_CONDITION];
+  if (Number.isFinite(maxAgeDays) && maxAgeDays > 0) {
+    const cutoffDate = new Date(Date.now() - Math.floor(maxAgeDays) * 24 * 60 * 60 * 1000);
+    conditions.push(gte(usageLedger.createdAt, cutoffDate));
+  }
+
   const rows = await db
     .select({
       userId: usageLedger.userId,
       total: sql<number>`COALESCE(SUM(${usageLedger.costUsd}), 0)`,
     })
     .from(usageLedger)
-    .where(and(inArray(usageLedger.userId, userIds), LEDGER_BILLING_CONDITION))
+    .where(and(...conditions))
     .groupBy(usageLedger.userId);
 
-  for (const id of userIds) {
-    result.set(id, 0);
-  }
-  for (const row of rows) {
-    result.set(row.userId, Number(row.total || 0));
-  }
+  for (const id of userIds) result.set(id, 0);
+  for (const row of rows) result.set(row.userId, Number(row.total || 0));
   return result;
 }
 
 /**
- * Batch query: all-time total cost grouped by key_id (single SQL query via JOIN)
+ * Batch query: total cost grouped by key_id using a two-step PK lookup then aggregate.
+ * Avoids varchar LEFT JOIN by first resolving key strings via PK, then aggregating on
+ * usage_ledger directly (hits idx_usage_ledger_key_cost index).
  * @param keyIds - Array of key IDs
+ * @param maxAgeDays - Only include records newer than this many days (default 365, use Infinity to include all)
  * @returns Map of keyId -> totalCost
  */
-export async function sumKeyTotalCostBatchByIds(keyIds: number[]): Promise<Map<number, number>> {
+export async function sumKeyTotalCostBatchByIds(
+  keyIds: number[],
+  maxAgeDays: number = 365
+): Promise<Map<number, number>> {
   const result = new Map<number, number>();
   if (keyIds.length === 0) return result;
+  for (const id of keyIds) result.set(id, 0);
+
+  // Step 1: PK lookup -> key strings
+  const keyMappings = await db
+    .select({ id: keys.id, key: keys.key })
+    .from(keys)
+    .where(inArray(keys.id, keyIds));
+
+  const keyStringToId = new Map(keyMappings.map((k) => [k.key, k.id]));
+  const keyStrings = keyMappings.map((k) => k.key);
+  if (keyStrings.length === 0) return result;
+
+  // Step 2: Aggregate on usage_ledger directly (hits idx_usage_ledger_key_cost)
+  const conditions: SQL[] = [inArray(usageLedger.key, keyStrings), LEDGER_BILLING_CONDITION];
+  if (Number.isFinite(maxAgeDays) && maxAgeDays > 0) {
+    const cutoffDate = new Date(Date.now() - Math.floor(maxAgeDays) * 24 * 60 * 60 * 1000);
+    conditions.push(gte(usageLedger.createdAt, cutoffDate));
+  }
 
   const rows = await db
     .select({
-      keyId: keys.id,
+      key: usageLedger.key,
       total: sql<number>`COALESCE(SUM(${usageLedger.costUsd}), 0)`,
     })
-    .from(keys)
-    .leftJoin(usageLedger, and(eq(usageLedger.key, keys.key), LEDGER_BILLING_CONDITION))
-    .where(inArray(keys.id, keyIds))
-    .groupBy(keys.id);
+    .from(usageLedger)
+    .where(and(...conditions))
+    .groupBy(usageLedger.key);
 
-  for (const id of keyIds) {
-    result.set(id, 0);
-  }
   for (const row of rows) {
-    result.set(row.keyId, Number(row.total || 0));
+    const keyId = keyStringToId.get(row.key);
+    if (keyId !== undefined) result.set(keyId, Number(row.total || 0));
   }
   return result;
 }

+ 12 - 3
src/repository/usage-ledger.ts

@@ -68,10 +68,13 @@ export async function sumLedgerTotalCost(
 /**
  * Batch total cost grouped by entity (single SQL query).
  * Returns Map of entityId (as string) -> totalCost.
+ * @param maxAgeDays - Only include ledger rows created within this many days (default 365).
+ *                     Pass Infinity or a non-positive number to include all-time records.
  */
 export async function sumLedgerTotalCostBatch(
   entityType: "user" | "key",
-  entityIds: number[] | string[]
+  entityIds: number[] | string[],
+  maxAgeDays: number = 365
 ): Promise<Map<string, string>> {
   const result = new Map<string, string>();
   if (entityIds.length === 0) return result;
@@ -80,6 +83,12 @@ export async function sumLedgerTotalCostBatch(
     result.set(String(id), "0");
   }
 
+  const timeConditions: ReturnType<typeof gte>[] = [];
+  if (Number.isFinite(maxAgeDays) && maxAgeDays > 0) {
+    const cutoffDate = new Date(Date.now() - Math.floor(maxAgeDays) * 24 * 60 * 60 * 1000);
+    timeConditions.push(gte(usageLedger.createdAt, cutoffDate));
+  }
+
   if (entityType === "user") {
     const ids = entityIds as number[];
     const rows = await db
@@ -88,7 +97,7 @@ export async function sumLedgerTotalCostBatch(
         total: sql<string>`COALESCE(SUM(${usageLedger.costUsd}), '0')`,
       })
       .from(usageLedger)
-      .where(and(inArray(usageLedger.userId, ids), LEDGER_BILLING_CONDITION))
+      .where(and(inArray(usageLedger.userId, ids), LEDGER_BILLING_CONDITION, ...timeConditions))
       .groupBy(usageLedger.userId);
     for (const row of rows) {
       result.set(String(row.entityId), row.total ?? "0");
@@ -101,7 +110,7 @@ export async function sumLedgerTotalCostBatch(
         total: sql<string>`COALESCE(SUM(${usageLedger.costUsd}), '0')`,
       })
       .from(usageLedger)
-      .where(and(inArray(usageLedger.key, ids), LEDGER_BILLING_CONDITION))
+      .where(and(inArray(usageLedger.key, ids), LEDGER_BILLING_CONDITION, ...timeConditions))
       .groupBy(usageLedger.key);
     for (const row of rows) {
       result.set(row.entityId, row.total ?? "0");

+ 8 - 0
src/repository/user.ts

@@ -59,6 +59,7 @@ export async function createUser(userData: CreateUserData): Promise<User> {
     isEnabled: userData.isEnabled ?? true,
     expiresAt: userData.expiresAt ?? null,
     allowedClients: userData.allowedClients ?? [],
+    blockedClients: userData.blockedClients ?? [],
     allowedModels: userData.allowedModels ?? [],
   };
 
@@ -84,6 +85,7 @@ export async function createUser(userData: CreateUserData): Promise<User> {
     isEnabled: users.isEnabled,
     expiresAt: users.expiresAt,
     allowedClients: users.allowedClients,
+    blockedClients: users.blockedClients,
     allowedModels: users.allowedModels,
   });
 
@@ -116,6 +118,7 @@ export async function findUserList(limit: number = 50, offset: number = 0): Prom
       isEnabled: users.isEnabled,
       expiresAt: users.expiresAt,
       allowedClients: users.allowedClients,
+      blockedClients: users.blockedClients,
       allowedModels: users.allowedModels,
     })
     .from(users)
@@ -294,6 +297,7 @@ export async function findUserListBatch(
       isEnabled: users.isEnabled,
       expiresAt: users.expiresAt,
       allowedClients: users.allowedClients,
+      blockedClients: users.blockedClients,
       allowedModels: users.allowedModels,
     })
     .from(users)
@@ -338,6 +342,7 @@ export async function findUserById(id: number): Promise<User | null> {
       isEnabled: users.isEnabled,
       expiresAt: users.expiresAt,
       allowedClients: users.allowedClients,
+      blockedClients: users.blockedClients,
       allowedModels: users.allowedModels,
     })
     .from(users)
@@ -371,6 +376,7 @@ export async function updateUser(id: number, userData: UpdateUserData): Promise<
     isEnabled?: boolean;
     expiresAt?: Date | null;
     allowedClients?: string[];
+    blockedClients?: string[];
     allowedModels?: string[];
   }
 
@@ -402,6 +408,7 @@ export async function updateUser(id: number, userData: UpdateUserData): Promise<
   if (userData.isEnabled !== undefined) dbData.isEnabled = userData.isEnabled;
   if (userData.expiresAt !== undefined) dbData.expiresAt = userData.expiresAt;
   if (userData.allowedClients !== undefined) dbData.allowedClients = userData.allowedClients;
+  if (userData.blockedClients !== undefined) dbData.blockedClients = userData.blockedClients;
   if (userData.allowedModels !== undefined) dbData.allowedModels = userData.allowedModels;
 
   const [user] = await db
@@ -430,6 +437,7 @@ export async function updateUser(id: number, userData: UpdateUserData): Promise<
       isEnabled: users.isEnabled,
       expiresAt: users.expiresAt,
       allowedClients: users.allowedClients,
+      blockedClients: users.blockedClients,
       allowedModels: users.allowedModels,
     });
 

+ 11 - 2
src/types/message.ts

@@ -34,7 +34,8 @@ export interface ProviderChainItem {
     | "client_error_non_retryable" // 不可重试的客户端错误(Prompt 超限、内容过滤、PDF 限制、Thinking 格式)
     | "http2_fallback" // HTTP/2 协议错误,回退到 HTTP/1.1(不切换供应商、不计入熔断器)
     | "endpoint_pool_exhausted" // 端点池耗尽(所有端点熔断或不可用,严格模式阻止降级)
-    | "vendor_type_all_timeout"; // 供应商类型全端点超时(524),触发 vendor-type 临时熔断
+    | "vendor_type_all_timeout" // 供应商类型全端点超时(524),触发 vendor-type 临时熔断
+    | "client_restriction_filtered"; // Provider skipped due to client restriction (neutral, no circuit breaker)
 
   // === 选择方法(细化) ===
   selectionMethod?:
@@ -171,8 +172,16 @@ export interface ProviderChainItem {
         | "type_mismatch"
         | "model_not_allowed"
         | "context_1m_disabled" // 供应商禁用了 1M 上下文功能
-        | "disabled";
+        | "disabled"
+        | "client_restriction"; // Provider filtered due to client restriction
       details?: string; // 额外信息(如费用:$15.2/$15)
+      clientRestrictionContext?: {
+        matchType: "blocklist_hit" | "allowlist_miss";
+        matchedPattern?: string;
+        detectedClient?: string;
+        providerAllowlist: string[];
+        providerBlocklist: string[];
+      };
     }>;
 
     // --- 优先级分层 ---

+ 16 - 0
src/types/provider.ts

@@ -65,6 +65,8 @@ export type ProviderBatchPatchField =
   | "group_tag"
   | "model_redirects"
   | "allowed_models"
+  | "allowed_clients"
+  | "blocked_clients"
   | "anthropic_thinking_budget_preference"
   | "anthropic_adaptive_thinking"
   // Routing
@@ -112,6 +114,8 @@ export interface ProviderBatchPatchDraft {
   group_tag?: ProviderPatchDraftInput<string>;
   model_redirects?: ProviderPatchDraftInput<Record<string, string>>;
   allowed_models?: ProviderPatchDraftInput<string[]>;
+  allowed_clients?: ProviderPatchDraftInput<string[]>;
+  blocked_clients?: ProviderPatchDraftInput<string[]>;
   anthropic_thinking_budget_preference?: ProviderPatchDraftInput<AnthropicThinkingBudgetPreference>;
   anthropic_adaptive_thinking?: ProviderPatchDraftInput<AnthropicAdaptiveThinkingConfig>;
   // Routing
@@ -160,6 +164,8 @@ export interface ProviderBatchPatch {
   group_tag: ProviderPatchOperation<string>;
   model_redirects: ProviderPatchOperation<Record<string, string>>;
   allowed_models: ProviderPatchOperation<string[]>;
+  allowed_clients: ProviderPatchOperation<string[]>;
+  blocked_clients: ProviderPatchOperation<string[]>;
   anthropic_thinking_budget_preference: ProviderPatchOperation<AnthropicThinkingBudgetPreference>;
   anthropic_adaptive_thinking: ProviderPatchOperation<AnthropicAdaptiveThinkingConfig>;
   // Routing
@@ -208,6 +214,8 @@ export interface ProviderBatchApplyUpdates {
   group_tag?: string | null;
   model_redirects?: Record<string, string> | null;
   allowed_models?: string[] | null;
+  allowed_clients?: string[];
+  blocked_clients?: string[];
   anthropic_thinking_budget_preference?: AnthropicThinkingBudgetPreference | null;
   anthropic_adaptive_thinking?: AnthropicAdaptiveThinkingConfig | null;
   // Routing
@@ -285,6 +293,8 @@ export interface Provider {
   // - 非 Anthropic 提供商:声明列表(提供商声称支持的模型,可选)
   // - null 或空数组:Anthropic 允许所有 claude 模型,非 Anthropic 允许任意模型
   allowedModels: string[] | null;
+  allowedClients: string[]; // Allowed client patterns (empty = no restriction)
+  blockedClients: string[]; // Blocked client patterns (blacklist, checked before allowedClients)
 
   // MCP 透传类型:控制是否启用 MCP 透传功能
   // 'none': 不启用(默认)
@@ -390,6 +400,8 @@ export interface ProviderDisplay {
   modelRedirects: Record<string, string> | null;
   // 模型列表(双重语义)
   allowedModels: string[] | null;
+  allowedClients: string[]; // Allowed client patterns (empty = no restriction)
+  blockedClients: string[]; // Blocked client patterns (blacklist, checked before allowedClients)
   // MCP 透传类型
   mcpPassthroughType: McpPassthroughType;
   // MCP 透传 URL
@@ -479,6 +491,8 @@ export interface CreateProviderData {
   preserve_client_ip?: boolean;
   model_redirects?: Record<string, string> | null;
   allowed_models?: string[] | null;
+  allowed_clients?: string[] | null;
+  blocked_clients?: string[] | null;
   mcp_passthrough_type?: McpPassthroughType;
   mcp_passthrough_url?: string | null;
 
@@ -553,6 +567,8 @@ export interface UpdateProviderData {
   preserve_client_ip?: boolean;
   model_redirects?: Record<string, string> | null;
   allowed_models?: string[] | null;
+  allowed_clients?: string[] | null;
+  blocked_clients?: string[] | null;
   mcp_passthrough_type?: McpPassthroughType;
   mcp_passthrough_url?: string | null;
 

+ 5 - 0
src/types/user.ts

@@ -27,6 +27,7 @@ export interface User {
   expiresAt?: Date | null; // 用户过期时间
   // Allowed clients (CLI/IDE restrictions)
   allowedClients?: string[]; // 允许的客户端模式(空数组=无限制)
+  blockedClients?: string[]; // Blocked client patterns (blacklist, checked before allowedClients)
   // Allowed models (AI model restrictions)
   allowedModels?: string[]; // 允许的AI模型(空数组=无限制)
 }
@@ -55,6 +56,7 @@ export interface CreateUserData {
   expiresAt?: Date | null;
   // Allowed clients (CLI/IDE restrictions)
   allowedClients?: string[];
+  blockedClients?: string[]; // Blocked client patterns (blacklist, checked before allowedClients)
   // Allowed models (AI model restrictions)
   allowedModels?: string[];
 }
@@ -83,6 +85,7 @@ export interface UpdateUserData {
   expiresAt?: Date | null;
   // Allowed clients (CLI/IDE restrictions)
   allowedClients?: string[];
+  blockedClients?: string[]; // Blocked client patterns (blacklist, checked before allowedClients)
   // Allowed models (AI model restrictions)
   allowedModels?: string[];
 }
@@ -156,6 +159,7 @@ export interface UserDisplay {
   expiresAt?: Date | null; // 用户过期时间
   // Allowed clients (CLI/IDE restrictions)
   allowedClients?: string[]; // 允许的客户端模式(空数组=无限制)
+  blockedClients?: string[]; // Blocked client patterns (blacklist, checked before allowedClients)
   // Allowed models (AI model restrictions)
   allowedModels?: string[]; // 允许的AI模型(空数组=无限制)
 }
@@ -173,6 +177,7 @@ export interface KeyDialogUserContext {
   limitTotalUsd?: number | null;
   limitConcurrentSessions?: number;
   allowedClients?: string[];
+  blockedClients?: string[]; // Blocked client patterns (blacklist, checked before allowedClients)
   allowedModels?: string[];
 }
 

+ 428 - 0
tests/unit/proxy/client-detector.test.ts

@@ -0,0 +1,428 @@
+import { describe, expect, test } from "vitest";
+import {
+  BUILTIN_CLIENT_KEYWORDS,
+  CLAUDE_CODE_KEYWORD_PREFIX,
+  detectClientFull,
+  isBuiltinKeyword,
+  isClientAllowed,
+  isClientAllowedDetailed,
+  matchClientPattern,
+} from "@/app/v1/_lib/proxy/client-detector";
+import type { ProxySession } from "@/app/v1/_lib/proxy/session";
+
+type SessionOptions = {
+  userAgent?: string | null;
+  xApp?: string | null;
+  dangerousBrowserAccess?: string | null;
+  betas?: unknown;
+};
+
+function createMockSession(options: SessionOptions = {}): ProxySession {
+  const headers = new Headers();
+  if (options.xApp !== undefined && options.xApp !== null) {
+    headers.set("x-app", options.xApp);
+  }
+  if (options.dangerousBrowserAccess !== undefined && options.dangerousBrowserAccess !== null) {
+    headers.set("anthropic-dangerous-direct-browser-access", options.dangerousBrowserAccess);
+  }
+
+  const message: Record<string, unknown> = {};
+  if ("betas" in options) {
+    message.betas = options.betas;
+  }
+
+  return {
+    userAgent: options.userAgent ?? null,
+    headers,
+    request: {
+      message,
+    },
+  } as unknown as ProxySession;
+}
+
+function createConfirmedClaudeCodeSession(userAgent: string): ProxySession {
+  return createMockSession({
+    userAgent,
+    xApp: "cli",
+    betas: ["claude-code-test"],
+  });
+}
+
+describe("client-detector", () => {
+  describe("constants", () => {
+    test("CLAUDE_CODE_KEYWORD_PREFIX should be claude-code", () => {
+      expect(CLAUDE_CODE_KEYWORD_PREFIX).toBe("claude-code");
+    });
+
+    test("BUILTIN_CLIENT_KEYWORDS should contain 7 items", () => {
+      expect(BUILTIN_CLIENT_KEYWORDS.size).toBe(7);
+    });
+  });
+
+  describe("isBuiltinKeyword", () => {
+    test.each([
+      "claude-code",
+      "claude-code-cli",
+      "claude-code-cli-sdk",
+      "claude-code-vscode",
+      "claude-code-sdk-ts",
+      "claude-code-sdk-py",
+      "claude-code-gh-action",
+    ])("should return true for builtin keyword: %s", (pattern) => {
+      expect(isBuiltinKeyword(pattern)).toBe(true);
+    });
+
+    test.each([
+      "gemini-cli",
+      "codex-cli",
+      "custom-pattern",
+    ])("should return false for non-builtin keyword: %s", (pattern) => {
+      expect(isBuiltinKeyword(pattern)).toBe(false);
+    });
+  });
+
+  describe("confirmClaudeCodeSignals via detectClientFull", () => {
+    test("should confirm when all 3 strong signals are present", () => {
+      const session = createMockSession({
+        userAgent: "claude-cli/1.0.0 (external, cli)",
+        xApp: "cli",
+        betas: ["claude-code-cache-control-20260101"],
+      });
+
+      const result = detectClientFull(session, "claude-code");
+      expect(result.hubConfirmed).toBe(true);
+      expect(result.signals).toEqual(["x-app-cli", "ua-prefix", "betas-claude-code"]);
+      expect(result.supplementary).toEqual([]);
+    });
+
+    test.each([
+      {
+        name: "missing x-app",
+        options: {
+          userAgent: "claude-cli/1.0.0 (external, cli)",
+          betas: ["claude-code-foo"],
+        },
+      },
+      {
+        name: "missing ua-prefix",
+        options: {
+          userAgent: "GeminiCLI/1.0",
+          xApp: "cli",
+          betas: ["claude-code-foo"],
+        },
+      },
+      {
+        name: "missing betas-claude-code",
+        options: {
+          userAgent: "claude-cli/1.0.0 (external, cli)",
+          xApp: "cli",
+          betas: ["not-claude-code"],
+        },
+      },
+    ])("should not confirm with only 2-of-3 signals: $name", ({ options }) => {
+      const session = createMockSession(options);
+      const result = detectClientFull(session, "claude-code");
+      expect(result.hubConfirmed).toBe(false);
+      expect(result.signals.length).toBe(2);
+    });
+
+    test("should not confirm with 0 strong signals", () => {
+      const session = createMockSession({ userAgent: "GeminiCLI/1.0", betas: "not-array" });
+      const result = detectClientFull(session, "claude-code");
+
+      expect(result.hubConfirmed).toBe(false);
+      expect(result.signals).toEqual([]);
+    });
+
+    test("should collect supplementary signal without counting it", () => {
+      const session = createMockSession({
+        userAgent: "claude-cli/1.0.0 (external, cli)",
+        xApp: "cli",
+        betas: ["not-claude-code"],
+        dangerousBrowserAccess: "true",
+      });
+
+      const result = detectClientFull(session, "claude-code");
+      expect(result.hubConfirmed).toBe(false);
+      expect(result.signals).toEqual(["x-app-cli", "ua-prefix"]);
+      expect(result.supplementary).toEqual(["dangerous-browser-access"]);
+    });
+  });
+
+  describe("extractSubClient via detectClientFull", () => {
+    test.each([
+      ["cli", "claude-code-cli"],
+      ["sdk-cli", "claude-code-cli-sdk"],
+      ["claude-vscode", "claude-code-vscode"],
+      ["sdk-ts", "claude-code-sdk-ts"],
+      ["sdk-py", "claude-code-sdk-py"],
+      ["claude-code-github-action", "claude-code-gh-action"],
+    ])("should map entrypoint %s to %s", (entrypoint, expectedSubClient) => {
+      const session = createConfirmedClaudeCodeSession(
+        `claude-cli/1.2.3 (external, ${entrypoint})`
+      );
+      const result = detectClientFull(session, "claude-code");
+
+      expect(result.hubConfirmed).toBe(true);
+      expect(result.subClient).toBe(expectedSubClient);
+    });
+
+    test("should return null for unknown entrypoint", () => {
+      const session = createConfirmedClaudeCodeSession(
+        "claude-cli/1.2.3 (external, unknown-entry)"
+      );
+      const result = detectClientFull(session, "claude-code");
+
+      expect(result.hubConfirmed).toBe(true);
+      expect(result.subClient).toBeNull();
+    });
+
+    test("should return null for malformed UA", () => {
+      const session = createConfirmedClaudeCodeSession("claude-cli 1.2.3 (external, cli)");
+      const result = detectClientFull(session, "claude-code");
+
+      expect(result.hubConfirmed).toBe(false);
+      expect(result.subClient).toBeNull();
+    });
+
+    test("should return null when UA has no parentheses section", () => {
+      const session = createMockSession({
+        userAgent: "claude-cli/1.2.3 external, cli",
+        xApp: "cli",
+        betas: ["claude-code-a"],
+      });
+      const result = detectClientFull(session, "claude-code");
+
+      expect(result.hubConfirmed).toBe(true);
+      expect(result.subClient).toBeNull();
+    });
+  });
+
+  describe("matchClientPattern builtin keyword path", () => {
+    test("should match wildcard claude-code when 3-of-3 is confirmed", () => {
+      const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)");
+      expect(matchClientPattern(session, "claude-code")).toBe(true);
+    });
+
+    test("should match claude-code-cli for cli entrypoint", () => {
+      const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)");
+      expect(matchClientPattern(session, "claude-code-cli")).toBe(true);
+    });
+
+    test("should match claude-code-vscode for claude-vscode entrypoint", () => {
+      const session = createConfirmedClaudeCodeSession(
+        "claude-cli/1.2.3 (external, claude-vscode, agent-sdk/0.1.0)"
+      );
+      expect(matchClientPattern(session, "claude-code-vscode")).toBe(true);
+    });
+
+    test("should return false when sub-client does not match", () => {
+      const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, sdk-py)");
+      expect(matchClientPattern(session, "claude-code-sdk-ts")).toBe(false);
+    });
+
+    test("should return false when only 2-of-3 signals are present", () => {
+      const session = createMockSession({
+        userAgent: "claude-cli/1.2.3 (external, cli)",
+        xApp: "cli",
+        betas: ["non-claude-code"],
+      });
+      expect(matchClientPattern(session, "claude-code")).toBe(false);
+    });
+  });
+
+  describe("matchClientPattern custom substring path", () => {
+    test("should match gemini-cli against GeminiCLI", () => {
+      const session = createMockSession({ userAgent: "GeminiCLI/1.0" });
+      expect(matchClientPattern(session, "gemini-cli")).toBe(true);
+    });
+
+    test("should match codex-cli against codex_cli", () => {
+      const session = createMockSession({ userAgent: "codex_cli/2.0" });
+      expect(matchClientPattern(session, "codex-cli")).toBe(true);
+    });
+
+    test("should return false when User-Agent is empty", () => {
+      const session = createMockSession({ userAgent: "   " });
+      expect(matchClientPattern(session, "gemini-cli")).toBe(false);
+    });
+
+    test("should return false when custom pattern is not found", () => {
+      const session = createMockSession({ userAgent: "Mozilla/5.0 Compatible" });
+      expect(matchClientPattern(session, "gemini-cli")).toBe(false);
+    });
+
+    test("should return false when pattern normalizes to empty", () => {
+      const session = createMockSession({ userAgent: "AnyClient/1.0" });
+      expect(matchClientPattern(session, "-_-")).toBe(false);
+    });
+  });
+
+  describe("isClientAllowed", () => {
+    test("should reject when blocked matches even if allowed also matches", () => {
+      const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)");
+      expect(isClientAllowed(session, ["claude-code"], ["claude-code"])).toBe(false);
+    });
+
+    test("should allow when allowedClients and blockedClients are both empty", () => {
+      const session = createMockSession({ userAgent: "AnyClient/1.0" });
+      expect(isClientAllowed(session, [], [])).toBe(true);
+    });
+
+    test("should allow when allowedClients match", () => {
+      const session = createMockSession({ userAgent: "GeminiCLI/1.0" });
+      expect(isClientAllowed(session, ["gemini-cli"])).toBe(true);
+    });
+
+    test("should reject when allowedClients are set but none match", () => {
+      const session = createMockSession({ userAgent: "UnknownClient/1.0" });
+      expect(isClientAllowed(session, ["gemini-cli"])).toBe(false);
+    });
+
+    test("should reject when only blockedClients are set and blocked matches", () => {
+      const session = createMockSession({ userAgent: "GeminiCLI/1.0" });
+      expect(isClientAllowed(session, [], ["gemini-cli"])).toBe(false);
+    });
+
+    test("should allow when only blockedClients are set and blocked does not match", () => {
+      const session = createMockSession({ userAgent: "GeminiCLI/1.0" });
+      expect(isClientAllowed(session, [], ["codex-cli"])).toBe(true);
+    });
+
+    test("should allow when blocked does not match and allowed matches", () => {
+      const session = createMockSession({ userAgent: "codex_cli/2.0" });
+      expect(isClientAllowed(session, ["codex-cli"], ["gemini-cli"])).toBe(true);
+    });
+  });
+
+  describe("isClientAllowedDetailed", () => {
+    test("should return no_restriction when both lists are empty", () => {
+      const session = createMockSession({ userAgent: "AnyClient/1.0" });
+      const result = isClientAllowedDetailed(session, [], []);
+      expect(result).toEqual({
+        allowed: true,
+        matchType: "no_restriction",
+        matchedPattern: undefined,
+        detectedClient: undefined,
+        checkedAllowlist: [],
+        checkedBlocklist: [],
+      });
+    });
+
+    test("should return blocklist_hit with matched pattern", () => {
+      const session = createMockSession({ userAgent: "GeminiCLI/1.0" });
+      const result = isClientAllowedDetailed(session, [], ["gemini-cli"]);
+      expect(result.allowed).toBe(false);
+      expect(result.matchType).toBe("blocklist_hit");
+      expect(result.matchedPattern).toBe("gemini-cli");
+      expect(result.detectedClient).toBe("GeminiCLI/1.0");
+      expect(result.checkedBlocklist).toEqual(["gemini-cli"]);
+    });
+
+    test("should return allowlist_miss when no allowlist pattern matches", () => {
+      const session = createMockSession({ userAgent: "UnknownClient/1.0" });
+      const result = isClientAllowedDetailed(session, ["gemini-cli", "codex-cli"], []);
+      expect(result.allowed).toBe(false);
+      expect(result.matchType).toBe("allowlist_miss");
+      expect(result.matchedPattern).toBeUndefined();
+      expect(result.detectedClient).toBe("UnknownClient/1.0");
+      expect(result.checkedAllowlist).toEqual(["gemini-cli", "codex-cli"]);
+    });
+
+    test("should return allowed when allowlist matches", () => {
+      const session = createMockSession({ userAgent: "GeminiCLI/1.0" });
+      const result = isClientAllowedDetailed(session, ["gemini-cli"], []);
+      expect(result.allowed).toBe(true);
+      expect(result.matchType).toBe("allowed");
+      expect(result.matchedPattern).toBe("gemini-cli");
+      expect(result.detectedClient).toBe("GeminiCLI/1.0");
+    });
+
+    test("blocklist takes precedence over allowlist", () => {
+      const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)");
+      const result = isClientAllowedDetailed(session, ["claude-code"], ["claude-code"]);
+      expect(result.allowed).toBe(false);
+      expect(result.matchType).toBe("blocklist_hit");
+      expect(result.matchedPattern).toBe("claude-code");
+    });
+
+    test("should detect sub-client for builtin keywords", () => {
+      const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, sdk-ts)");
+      const result = isClientAllowedDetailed(session, ["claude-code"], []);
+      expect(result.allowed).toBe(true);
+      expect(result.matchType).toBe("allowed");
+      expect(result.detectedClient).toBe("claude-code-sdk-ts");
+      expect(result.matchedPattern).toBe("claude-code");
+    });
+
+    test("should return allowed when only blocklist set and no match", () => {
+      const session = createMockSession({ userAgent: "CodexCLI/1.0" });
+      const result = isClientAllowedDetailed(session, [], ["gemini-cli"]);
+      expect(result.allowed).toBe(true);
+      expect(result.matchType).toBe("allowed");
+      expect(result.detectedClient).toBe("CodexCLI/1.0");
+    });
+
+    test("should return no_restriction when blockedClients is undefined and allowlist empty", () => {
+      const session = createMockSession({ userAgent: "AnyClient/1.0" });
+      const result = isClientAllowedDetailed(session, []);
+      expect(result.allowed).toBe(true);
+      expect(result.matchType).toBe("no_restriction");
+    });
+
+    test("should capture first matching blocked pattern", () => {
+      const session = createMockSession({ userAgent: "GeminiCLI/1.0" });
+      const result = isClientAllowedDetailed(
+        session,
+        [],
+        ["codex-cli", "gemini-cli", "factory-cli"]
+      );
+      expect(result.allowed).toBe(false);
+      expect(result.matchType).toBe("blocklist_hit");
+      expect(result.matchedPattern).toBe("gemini-cli");
+    });
+  });
+
+  describe("detectClientFull", () => {
+    test("should return matched=true for confirmed claude-code wildcard", () => {
+      const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, sdk-ts)");
+      const result = detectClientFull(session, "claude-code");
+
+      expect(result).toEqual({
+        matched: true,
+        hubConfirmed: true,
+        subClient: "claude-code-sdk-ts",
+        signals: ["x-app-cli", "ua-prefix", "betas-claude-code"],
+        supplementary: [],
+      });
+    });
+
+    test("should return matched=false for confirmed but different builtin sub-client", () => {
+      const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, sdk-ts)");
+      const result = detectClientFull(session, "claude-code-cli");
+
+      expect(result.hubConfirmed).toBe(true);
+      expect(result.subClient).toBe("claude-code-sdk-ts");
+      expect(result.matched).toBe(false);
+    });
+
+    test("should use custom normalization path for non-builtin patterns", () => {
+      const session = createMockSession({ userAgent: "GeminiCLI/0.22.5" });
+      const result = detectClientFull(session, "gemini-cli");
+
+      expect(result.matched).toBe(true);
+      expect(result.hubConfirmed).toBe(false);
+      expect(result.subClient).toBeNull();
+    });
+
+    test("should return matched=false for custom pattern when User-Agent is missing", () => {
+      const session = createMockSession({ userAgent: null });
+      const result = detectClientFull(session, "gemini-cli");
+
+      expect(result.matched).toBe(false);
+      expect(result.hubConfirmed).toBe(false);
+      expect(result.signals).toEqual([]);
+      expect(result.supplementary).toEqual([]);
+    });
+  });
+});

+ 51 - 2
tests/unit/proxy/client-guard.test.ts

@@ -1,4 +1,4 @@
-import { describe, expect, test, vi, beforeEach } from "vitest";
+import { describe, expect, test, vi } from "vitest";
 import { ProxyClientGuard } from "@/app/v1/_lib/proxy/client-guard";
 import type { ProxySession } from "@/app/v1/_lib/proxy/session";
 
@@ -13,13 +13,17 @@ vi.mock("@/app/v1/_lib/proxy/responses", () => ({
 // Helper to create mock session
 function createMockSession(
   userAgent: string | undefined,
-  allowedClients: string[] = []
+  allowedClients: string[] = [],
+  blockedClients: string[] = []
 ): ProxySession {
   return {
     userAgent,
+    headers: new Headers(),
+    request: { message: {} },
     authState: {
       user: {
         allowedClients,
+        blockedClients,
       },
     },
   } as unknown as ProxySession;
@@ -57,6 +61,14 @@ describe("ProxyClientGuard", () => {
     });
   });
 
+  describe("when both allowedClients and blockedClients are empty", () => {
+    test("should allow request", async () => {
+      const session = createMockSession("AnyClient/1.0", [], []);
+      const result = await ProxyClientGuard.ensure(session);
+      expect(result).toBeNull();
+    });
+  });
+
   describe("when restrictions are configured", () => {
     test("should reject when User-Agent is missing", async () => {
       const session = createMockSession(undefined, ["claude-cli"]);
@@ -196,4 +208,41 @@ describe("ProxyClientGuard", () => {
       expect(result).toBeNull();
     });
   });
+
+  describe("when blockedClients is configured", () => {
+    test("should reject when client matches blocked pattern", async () => {
+      const session = createMockSession("GeminiCLI/1.0", [], ["gemini-cli"]);
+      const result = await ProxyClientGuard.ensure(session);
+      expect(result).not.toBeNull();
+      expect(result?.status).toBe(400);
+    });
+
+    test("should allow when client does not match blocked pattern", async () => {
+      const session = createMockSession("CodexCLI/1.0", [], ["gemini-cli"]);
+      const result = await ProxyClientGuard.ensure(session);
+      expect(result).toBeNull();
+    });
+
+    test("should reject even when allowedClients matches", async () => {
+      const session = createMockSession("gemini-cli/1.0", ["gemini-cli"], ["gemini-cli"]);
+      const result = await ProxyClientGuard.ensure(session);
+      expect(result).not.toBeNull();
+      expect(result?.status).toBe(400);
+    });
+  });
+
+  describe("when only blockedClients is configured (no allowedClients)", () => {
+    test("should reject matching client", async () => {
+      const session = createMockSession("codex-cli/2.0", [], ["codex-cli"]);
+      const result = await ProxyClientGuard.ensure(session);
+      expect(result).not.toBeNull();
+      expect(result?.status).toBe(400);
+    });
+
+    test("should allow non-matching client", async () => {
+      const session = createMockSession("claude-cli/1.0", [], ["codex-cli"]);
+      const result = await ProxyClientGuard.ensure(session);
+      expect(result).toBeNull();
+    });
+  });
 });

+ 6 - 0
tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx

@@ -304,6 +304,12 @@ describe("ProviderForm: endpoint pool integration", () => {
     }
 
     expect(providersActionMocks.addProvider).toHaveBeenCalledTimes(1);
+    expect(providersActionMocks.addProvider).toHaveBeenCalledWith(
+      expect.objectContaining({
+        allowed_clients: [],
+        blocked_clients: [],
+      })
+    );
 
     await flushTicks(3);
     expect(providerEndpointsActionMocks.addProviderEndpoint).toHaveBeenCalledTimes(0);

+ 13 - 1
tests/unit/user-dialogs.test.tsx

@@ -256,6 +256,14 @@ const messages = {
             description: "Restrict clients",
             customLabel: "Custom",
             customPlaceholder: "Custom client",
+            customHelp: "Custom help",
+          },
+          blockedClients: {
+            label: "Blocked Clients",
+            description: "Blocked description",
+            customLabel: "Custom blocked",
+            customPlaceholder: "Blocked client",
+            customHelp: "Blocked help",
           },
           allowedModels: {
             label: "Allowed Models",
@@ -263,8 +271,12 @@ const messages = {
             description: "Restrict models",
           },
         },
+        actions: {
+          allow: "Allow",
+          block: "Block",
+        },
         presetClients: {
-          "claude-cli": "Claude CLI",
+          "claude-code": "Claude Code",
           "gemini-cli": "Gemini CLI",
           "factory-cli": "Factory CLI",
           "codex-cli": "Codex CLI",