Browse Source

feat: Dashboard Logs:秒级时间筛选 + Session ID 精确筛选/联想/展示(含回归修复) (#611)

* feat: add sessionId filter for usage logs

* feat: add seconds-level time filters for logs

* feat: wire sessionId into logs URL filters

* chore: add i18n keys for logs sessionId

* feat: add sessionId column to logs tables

* feat: add sessionId suggestions for logs

* docs: document dashboard logs call chain

* test: add logs sessionId/time filter coverage config

* fix: keep sessionId search input focused

* fix: drop leaked page param on logs apply

* fix: reload sessionId suggestions on scope change

* fix: harden logs url params and time parsing

* fix: avoid keys join for sessionId suggestions

* test: strengthen empty sessionId filter assertions

* chore: format logs sessionId suggestions test

* Update src/actions/usage-logs.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* feat: 补充公共静态变量 SESSION_ID_SUGGESTION_LIMIT

* fix: use prefix LIKE for sessionId suggestions

* refactor: centralize usage logs sessionId suggestion constants

* refactor: simplify logs url filters parsing

* refactor: reuse clipboard util for sessionId copy

* chore(db): add sessionId prefix index

* docs: clarify sessionId suggestion semantics

* test: add escapeLike unit tests

* chore: apply biome fixes for sessionId search

* feat: include session id in error responses

* test: add coverage suite for session id errors

* docs: add guide for error session id

* chore: format code (feat-logs-sessionid-time-filter-233f96a)
YangQing-Lin 3 weeks ago
parent
commit
d9e85acdac
47 changed files with 5225 additions and 183 deletions
  1. 2 0
      .gitignore
  2. 119 0
      docs/dashboard-logs-callchain.md
  3. 26 0
      docs/error-session-id-guide.md
  4. 1 0
      drizzle/0055_neat_stepford_cuckoos.sql
  5. 2404 0
      drizzle/meta/0055_snapshot.json
  6. 7 0
      drizzle/meta/_journal.json
  7. 4 0
      messages/en/dashboard.json
  8. 4 0
      messages/ja/dashboard.json
  9. 4 0
      messages/ru/dashboard.json
  10. 4 0
      messages/zh-CN/dashboard.json
  11. 4 0
      messages/zh-TW/dashboard.json
  12. 2 0
      package.json
  13. 53 0
      src/actions/usage-logs.ts
  14. 258 29
      src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx
  15. 1 0
      src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx
  16. 56 0
      src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx
  17. 42 2
      src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
  18. 30 52
      src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx
  19. 5 52
      src/app/[locale]/dashboard/logs/_components/usage-logs-view.tsx
  20. 49 1
      src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
  21. 93 0
      src/app/[locale]/dashboard/logs/_utils/logs-query.ts
  22. 49 0
      src/app/[locale]/dashboard/logs/_utils/time-range.ts
  23. 17 11
      src/app/v1/_lib/codex/chat-completions-handler.ts
  24. 6 2
      src/app/v1/_lib/proxy-handler.ts
  25. 39 23
      src/app/v1/_lib/proxy/error-handler.ts
  26. 56 0
      src/app/v1/_lib/proxy/error-session-id.ts
  27. 2 0
      src/drizzle/schema.ts
  28. 3 0
      src/lib/constants/usage-logs.constants.ts
  29. 39 11
      src/lib/utils/clipboard.ts
  30. 3 0
      src/repository/_shared/like.ts
  31. 79 0
      src/repository/usage-logs.ts
  32. 128 0
      tests/unit/dashboard-logs-filters-time-range.test.tsx
  33. 90 0
      tests/unit/dashboard-logs-query-utils.test.ts
  34. 322 0
      tests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsx
  35. 46 0
      tests/unit/dashboard-logs-time-range-utils.test.ts
  36. 15 0
      tests/unit/lib/constants/usage-logs.constants.test.ts
  37. 119 0
      tests/unit/lib/utils/clipboard.test.ts
  38. 22 0
      tests/unit/proxy/chat-completions-handler-guard-pipeline.test.ts
  39. 24 0
      tests/unit/proxy/error-handler-session-id-error.test.ts
  40. 207 0
      tests/unit/proxy/proxy-handler-session-id-error.test.ts
  41. 75 0
      tests/unit/proxy/responses-session-id.test.ts
  42. 23 0
      tests/unit/repository/escape-like.test.ts
  43. 309 0
      tests/unit/repository/usage-logs-sessionid-filter.test.ts
  44. 204 0
      tests/unit/repository/usage-logs-sessionid-suggestions.test.ts
  45. 59 0
      vitest.include-session-id-in-errors.config.ts
  46. 61 0
      vitest.logs-sessionid-time-filter.config.ts
  47. 60 0
      vitest.usage-logs-sessionid-search.config.ts

+ 2 - 0
.gitignore

@@ -16,6 +16,8 @@
 /coverage-my-usage
 /coverage-proxy-guard-pipeline
 /coverage-thinking-signature-rectifier
+/coverage-logs-sessionid-time-filter
+/coverage-usage-logs-sessionid-search
 
 # next.js
 /.next/

+ 119 - 0
docs/dashboard-logs-callchain.md

@@ -0,0 +1,119 @@
+# Dashboard Logs(Usage Logs)入口与调用链盘点
+
+本文用于锁定 `/dashboard/logs` 的真实入口与关键调用链边界,避免后续需求实现与验收口径跑偏。
+
+## 1) 路由入口(Server)
+
+- 路由:`/dashboard/logs`
+- 入口页面:`src/app/[locale]/dashboard/logs/page.tsx`
+  - 登录态校验:`getSession()`(未登录重定向到 `/login`)
+  - 数据区块入口:`UsageLogsDataSection`(`src/app/[locale]/dashboard/logs/_components/usage-logs-sections.tsx`)
+
+## 2) 真实渲染链路(Client)
+
+当前页面实际使用“虚拟列表”链路:
+
+- 虚拟列表入口:`UsageLogsViewVirtualized`(`src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx`)
+  - URL -> filters 解析:`parseLogsUrlFilters()`(`src/app/[locale]/dashboard/logs/_utils/logs-query.ts`)
+  - filters -> URL 回填:`buildLogsUrlQuery()`(`src/app/[locale]/dashboard/logs/_utils/logs-query.ts`)
+  - Filters 面板:`UsageLogsFilters`
+  - 列表:`VirtualizedLogsTable`
+  - 统计面板:`UsageLogsStatsPanel`
+
+仓库内仍存在“非虚拟表格”实现(目前不被路由引用,属于历史/备用路径):
+
+- `UsageLogsView`(`src/app/[locale]/dashboard/logs/_components/usage-logs-view.tsx`)
+- `UsageLogsTable`(`src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx`)
+
+## 3) 过滤器 / URL / 时间语义
+
+- URL 参数解析/构建(统一入口):`src/app/[locale]/dashboard/logs/_utils/logs-query.ts`
+  - `sessionId`:字符串(trim 后空值不落盘)
+  - `startTime/endTime`:毫秒时间戳
+- 秒级时间工具:`src/app/[locale]/dashboard/logs/_utils/time-range.ts`
+  - UI endTime 为“包含式”秒;对后端转换为“排他上界”(`endExclusive = endInclusive + 1s`)
+  - 后端查询语义保持:`created_at >= startTime` 且 `created_at < endTime`
+
+## 4) 数据获取链路(Actions -> Repository)
+
+### 列表(无限滚动)
+
+- Action:`src/actions/usage-logs.ts#getUsageLogsBatch`
+- Repo:`src/repository/usage-logs.ts#findUsageLogsBatch`
+
+### 统计(折叠面板按需加载)
+
+- Action:`src/actions/usage-logs.ts#getUsageLogsStats`
+- Repo:`src/repository/usage-logs.ts#findUsageLogsStats`
+
+### 导出 CSV
+
+- Action:`src/actions/usage-logs.ts#exportUsageLogs`
+- Repo:`src/repository/usage-logs.ts#findUsageLogsWithDetails`
+- CSV 生成:`src/actions/usage-logs.ts#generateCsv`
+
+### Session ID 联想(候选查询)
+
+- Action:`src/actions/usage-logs.ts#getUsageLogSessionIdSuggestions`
+- Repo:`src/repository/usage-logs.ts#findUsageLogSessionIdSuggestions`
+
+#### 匹配语义与边界(2026-01-15 更新)
+
+- **前端约束**:
+  - 最小长度:`SESSION_ID_SUGGESTION_MIN_LEN`(`src/lib/constants/usage-logs.constants.ts`)
+  - 最大长度截断:`SESSION_ID_SUGGESTION_MAX_LEN`(`src/actions/usage-logs.ts` 内对输入 trim 后截断)
+  - 每次返回数量:`SESSION_ID_SUGGESTION_LIMIT`
+- **后端匹配**:
+  - 语义:仅支持「字面量前缀匹配」(`term%`),不再支持包含匹配(`%term%`)
+  - 安全:输入中的 `%` / `_` / `\\` 会被统一转义,避免被当作 LIKE 通配符
+  - SQL(核心条件):`session_id LIKE '<escapedTerm>%' ESCAPE '\\'`
+    - 转义实现:`src/repository/_shared/like.ts#escapeLike`
+- **行为变更示例**:
+  - 之前:输入 `abc` 可能命中 `xxxabcxxx`(包含匹配)
+  - 之后:仅命中 `abc...`(前缀匹配)
+  - 之前:输入 `%` / `_` 可主动触发通配
+  - 之后:`%` / `_` 按字面量处理(例如输入 `%a` 只匹配以 `%a` 开头的 session_id)
+
+#### 索引与迁移(前缀匹配性能)
+
+- 已有索引:`idx_message_request_session_id`(`message_request.session_id`,partial: `deleted_at IS NULL`)
+- 新增索引(前缀匹配):`idx_message_request_session_id_prefix`
+  - opclass:`varchar_pattern_ops`
+  - partial:`deleted_at IS NULL AND (blocked_by IS NULL OR blocked_by <> 'warmup')`
+  - 迁移文件:`drizzle/0055_neat_stepford_cuckoos.sql`
+
+## 5) 本需求相关影响面(文件/符号清单)
+
+**前端(logs 页面内聚)**:
+
+- URL/过滤器:`src/app/[locale]/dashboard/logs/_utils/logs-query.ts`
+- 秒级时间:`src/app/[locale]/dashboard/logs/_utils/time-range.ts`
+- 过滤器 UI:`src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx`
+- 虚拟列表:`src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx`
+- 非虚拟表格:`src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx`
+- 统计面板:`src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx`
+
+**后端(Actions/Repo)**:
+
+- Actions:`src/actions/usage-logs.ts`
+  - `getUsageLogsBatch/getUsageLogsStats/exportUsageLogs/getUsageLogSessionIdSuggestions`
+- Repo:`src/repository/usage-logs.ts`
+  - `findUsageLogsBatch/findUsageLogsWithDetails/findUsageLogsStats/findUsageLogSessionIdSuggestions`
+
+**i18n(用户可见文案)**:
+
+- `messages/*/dashboard.json`(`dashboard.logs.filters.*` / `dashboard.logs.columns.*`)
+
+## 6) 边界说明(在范围内 / 不在范围内)
+
+在范围内(本次需求直接相关):
+
+- `sessionId` 精确筛选 + URL 回填 + UI 展示(列/复制/tooltip)
+- 秒级时间输入与 `endExclusive` 语义对齐(`< endTime`)
+- Session ID 联想(最小成本:minLen + debounce + limit)
+
+不在范围内(需另开 issue/评审确认后再做):
+
+- 针对联想查询的索引/物化/离线表(优化类工程)
+- 大规模改动数据库 schema 或重建索引策略(例如 CONCURRENTLY/离线重建)
+- Logs 页面其它过滤项语义调整(非本需求验收口径)

+ 26 - 0
docs/error-session-id-guide.md

@@ -0,0 +1,26 @@
+# Error Session ID Guide
+
+When reporting an API error, include the CCH session id so maintainers can locate the exact request.
+
+## Where to find it
+
+1. **Preferred**: response header `x-cch-session-id`
+2. **Fallback**: `error.message` suffix `cch_session_id: <id>`
+
+If the response does not include a session id, the server could not determine it for that request.
+
+## Example (curl)
+
+```bash
+curl -i -sS \\
+  -H "Authorization: Bearer <your-key>" \\
+  -H "Content-Type: application/json" \\
+  -d '{"model":"gpt-4.1-mini","messages":[{"role":"user","content":"hi"}]}' \\
+  http://localhost:13500/v1/chat/completions
+```
+
+In the response:
+
+- Check header: `x-cch-session-id: ...`
+- If missing, check JSON: `{"error":{"message":"... (cch_session_id: ...)"} }`
+

+ 1 - 0
drizzle/0055_neat_stepford_cuckoos.sql

@@ -0,0 +1 @@
+CREATE INDEX IF NOT EXISTS "idx_message_request_session_id_prefix" ON "message_request" USING btree ("session_id" varchar_pattern_ops) WHERE "message_request"."deleted_at" IS NULL AND ("message_request"."blocked_by" IS NULL OR "message_request"."blocked_by" <> 'warmup');

+ 2404 - 0
drizzle/meta/0055_snapshot.json

@@ -0,0 +1,2404 @@
+{
+  "id": "b40c930a-4001-4403-90b9-652a5878893c",
+  "prevId": "36887729-08df-4af3-98fe-d4fa87c7c5c7",
+  "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",
+          "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_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": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "integer",
+          "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": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_read_input_tokens": {
+          "name": "cache_read_input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_5m_input_tokens": {
+          "name": "cache_creation_5m_input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_1h_input_tokens": {
+          "name": "cache_creation_1h_input_tokens",
+          "type": "integer",
+          "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
+        },
+        "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_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_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_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": {}
+        }
+      },
+      "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,
+          "default": "'Asia/Shanghai'"
+        },
+        "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.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
+        },
+        "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
+        },
+        "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"
+        },
+        "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
+        },
+        "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
+        },
+        "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_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": {}
+        }
+      },
+      "foreignKeys": {},
+      "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'"
+        },
+        "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_codex_session_id_completion": {
+          "name": "enable_codex_session_id_completion",
+          "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"
+        },
+        "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.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"
+        },
+        "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_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": {}
+  }
+}

+ 7 - 0
drizzle/meta/_journal.json

@@ -386,6 +386,13 @@
       "when": 1768240715707,
       "tag": "0054_tidy_winter_soldier",
       "breakpoints": true
+    },
+    {
+      "idx": 55,
+      "version": "7",
+      "when": 1768443427816,
+      "tag": "0055_neat_stepford_cuckoos",
+      "breakpoints": true
     }
   ]
 }

+ 4 - 0
messages/en/dashboard.json

@@ -60,10 +60,13 @@
     "filters": {
       "user": "User",
       "provider": "Provider",
+      "sessionId": "Session ID",
       "searchUser": "Search users...",
       "searchProvider": "Search providers...",
+      "searchSessionId": "Search session IDs...",
       "noUserFound": "No matching users found",
       "noProviderFound": "No matching providers found",
+      "noSessionFound": "No matching session IDs found",
       "model": "Model",
       "endpoint": "Endpoint",
       "status": "Status",
@@ -96,6 +99,7 @@
       "time": "Time",
       "user": "User",
       "key": "Key",
+      "sessionId": "Session ID",
       "provider": "Provider",
       "model": "Billing Model",
       "endpoint": "Endpoint",

+ 4 - 0
messages/ja/dashboard.json

@@ -60,10 +60,13 @@
     "filters": {
       "user": "ユーザー",
       "provider": "プロバイダー",
+      "sessionId": "セッションID",
       "searchUser": "ユーザーを検索...",
       "searchProvider": "プロバイダーを検索...",
+      "searchSessionId": "セッションIDを検索...",
       "noUserFound": "一致するユーザーが見つかりません",
       "noProviderFound": "一致するプロバイダーが見つかりません",
+      "noSessionFound": "一致するセッションIDが見つかりません",
       "model": "モデル",
       "endpoint": "エンドポイント",
       "status": "ステータス",
@@ -96,6 +99,7 @@
       "time": "時間",
       "user": "ユーザー",
       "key": "キー",
+      "sessionId": "セッションID",
       "provider": "プロバイダー",
       "model": "課金モデル",
       "endpoint": "エンドポイント",

+ 4 - 0
messages/ru/dashboard.json

@@ -60,10 +60,13 @@
     "filters": {
       "user": "Пользователь",
       "provider": "Поставщик",
+      "sessionId": "ID сессии",
       "searchUser": "Поиск пользователей...",
       "searchProvider": "Поиск провайдеров...",
+      "searchSessionId": "Поиск ID сессии...",
       "noUserFound": "Пользователи не найдены",
       "noProviderFound": "Провайдеры не найдены",
+      "noSessionFound": "ID сессии не найдены",
       "model": "Модель",
       "endpoint": "Эндпоинт",
       "status": "Статус",
@@ -96,6 +99,7 @@
       "time": "Время",
       "user": "Пользователь",
       "key": "Ключ",
+      "sessionId": "ID сессии",
       "provider": "Поставщик",
       "model": "Модель тарификации",
       "endpoint": "Эндпоинт",

+ 4 - 0
messages/zh-CN/dashboard.json

@@ -60,10 +60,13 @@
     "filters": {
       "user": "用户",
       "provider": "供应商",
+      "sessionId": "Session ID",
       "searchUser": "搜索用户...",
       "searchProvider": "搜索供应商...",
+      "searchSessionId": "搜索 Session ID...",
       "noUserFound": "未找到匹配的用户",
       "noProviderFound": "未找到匹配的供应商",
+      "noSessionFound": "未找到匹配的 Session ID",
       "model": "模型",
       "endpoint": "端点",
       "status": "状态",
@@ -96,6 +99,7 @@
       "time": "时间",
       "user": "用户",
       "key": "密钥",
+      "sessionId": "Session ID",
       "provider": "供应商",
       "model": "计费模型",
       "endpoint": "端点",

+ 4 - 0
messages/zh-TW/dashboard.json

@@ -60,10 +60,13 @@
     "filters": {
       "user": "使用者",
       "provider": "供應商",
+      "sessionId": "Session ID",
       "searchUser": "搜尋使用者...",
       "searchProvider": "搜尋供應商...",
+      "searchSessionId": "搜尋 Session ID...",
       "noUserFound": "未找到匹配的使用者",
       "noProviderFound": "未找到匹配的供應商",
+      "noSessionFound": "未找到匹配的 Session ID",
       "model": "Model",
       "endpoint": "端點",
       "status": "狀態",
@@ -96,6 +99,7 @@
       "time": "時間",
       "user": "使用者",
       "key": "金鑰",
+      "sessionId": "Session ID",
       "provider": "供應商",
       "model": "計費模型",
       "endpoint": "端點",

+ 2 - 0
package.json

@@ -18,11 +18,13 @@
     "test:e2e": "vitest run --config vitest.e2e.config.ts --reporter=verbose",
     "test:integration": "vitest run --config vitest.integration.config.ts --reporter=verbose",
     "test:coverage": "vitest run --coverage",
+    "test:coverage:logs-sessionid-time-filter": "vitest run --config vitest.logs-sessionid-time-filter.config.ts --coverage",
     "test:coverage:codex-session-id-completer": "vitest run --config vitest.codex-session-id-completer.config.ts --coverage",
     "test:coverage:thinking-signature-rectifier": "vitest run --config vitest.thinking-signature-rectifier.config.ts --coverage",
     "test:coverage:quota": "vitest run --config vitest.quota.config.ts --coverage",
     "test:coverage:my-usage": "vitest run --config vitest.my-usage.config.ts --coverage",
     "test:coverage:proxy-guard-pipeline": "vitest run --config vitest.proxy-guard-pipeline.config.ts --coverage",
+    "test:coverage:include-session-id-in-errors": "vitest run --config vitest.include-session-id-in-errors.config.ts --coverage",
     "test:ci": "vitest run --reporter=default --reporter=junit --outputFile.junit=reports/vitest-junit.xml",
     "cui": "npx cui-server --host 0.0.0.0 --port 30000 --token a7564bc8882aa9a2d25d8b4ea6ea1e2e",
     "db:generate": "drizzle-kit generate && node scripts/validate-migrations.js",

+ 53 - 0
src/actions/usage-logs.ts

@@ -1,8 +1,14 @@
 "use server";
 
 import { getSession } from "@/lib/auth";
+import {
+  SESSION_ID_SUGGESTION_LIMIT,
+  SESSION_ID_SUGGESTION_MAX_LEN,
+  SESSION_ID_SUGGESTION_MIN_LEN,
+} from "@/lib/constants/usage-logs.constants";
 import { logger } from "@/lib/logger";
 import {
+  findUsageLogSessionIdSuggestions,
   findUsageLogsBatch,
   findUsageLogsStats,
   findUsageLogsWithDetails,
@@ -279,6 +285,53 @@ export async function getFilterOptions(): Promise<ActionResult<FilterOptions>> {
   }
 }
 
+export interface UsageLogSessionIdSuggestionInput {
+  term: string;
+  userId?: number;
+  keyId?: number;
+  providerId?: number;
+}
+
+export async function getUsageLogSessionIdSuggestions(
+  input: UsageLogSessionIdSuggestionInput
+): Promise<ActionResult<string[]>> {
+  try {
+    const session = await getSession();
+    if (!session) {
+      return { ok: false, error: "未登录" };
+    }
+
+    const trimmedTerm = input.term.trim().slice(0, SESSION_ID_SUGGESTION_MAX_LEN);
+    if (trimmedTerm.length < SESSION_ID_SUGGESTION_MIN_LEN) {
+      return { ok: true, data: [] };
+    }
+
+    const finalFilters =
+      session.user.role === "admin"
+        ? {
+            term: trimmedTerm,
+            userId: input.userId,
+            keyId: input.keyId,
+            providerId: input.providerId,
+            limit: SESSION_ID_SUGGESTION_LIMIT,
+          }
+        : {
+            term: trimmedTerm,
+            userId: session.user.id,
+            keyId: input.keyId,
+            providerId: input.providerId,
+            limit: SESSION_ID_SUGGESTION_LIMIT,
+          };
+
+    const sessionIds = await findUsageLogSessionIdSuggestions(finalFilters);
+    return { ok: true, data: sessionIds };
+  } catch (error) {
+    logger.error("获取 sessionId 联想失败:", error);
+    const message = error instanceof Error ? error.message : "获取 sessionId 联想失败";
+    return { ok: false, error: message };
+  }
+}
+
 /**
  * 获取使用日志聚合统计(独立接口,用于可折叠面板按需加载)
  *

+ 258 - 29
src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx

@@ -1,13 +1,13 @@
 "use client";
 
-import { addDays, format, parse } from "date-fns";
+import { format } from "date-fns";
 import { Check, ChevronsUpDown, Download } from "lucide-react";
 import { useTranslations } from "next-intl";
 
 import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 import { toast } from "sonner";
 import { getKeys } from "@/actions/keys";
-import { exportUsageLogs } from "@/actions/usage-logs";
+import { exportUsageLogs, getUsageLogSessionIdSuggestions } from "@/actions/usage-logs";
 import { searchUsersForFilter } from "@/actions/users";
 import { Button } from "@/components/ui/button";
 import {
@@ -20,7 +20,7 @@ import {
 } from "@/components/ui/command";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
 import {
   Select,
   SelectContent,
@@ -28,6 +28,7 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
+import { SESSION_ID_SUGGESTION_MIN_LEN } from "@/lib/constants/usage-logs.constants";
 import { useDebounce } from "@/lib/hooks/use-debounce";
 import type { Key } from "@/types/key";
 import type { ProviderDisplay } from "@/types/provider";
@@ -36,6 +37,11 @@ import {
   useLazyModels,
   useLazyStatusCodes,
 } from "../_hooks/use-lazy-filter-options";
+import {
+  dateStringWithClockToTimestamp,
+  formatClockFromTimestamp,
+  inclusiveEndTimestampFromExclusive,
+} from "../_utils/time-range";
 import { LogsDateRangePicker } from "./logs-date-range-picker";
 
 // 硬编码常用状态码(首次渲染时显示,无需等待加载)
@@ -51,6 +57,7 @@ interface UsageLogsFiltersProps {
     userId?: number;
     keyId?: number;
     providerId?: number;
+    sessionId?: string;
     /** 开始时间戳(毫秒,浏览器本地时区的 00:00:00) */
     startTime?: number;
     /** 结束时间戳(毫秒,浏览器本地时区的次日 00:00:00,用于 < 比较) */
@@ -125,6 +132,12 @@ export function UsageLogsFilters({
   const [isExporting, setIsExporting] = useState(false);
   const [userPopoverOpen, setUserPopoverOpen] = useState(false);
   const [providerPopoverOpen, setProviderPopoverOpen] = useState(false);
+  const [sessionIdPopoverOpen, setSessionIdPopoverOpen] = useState(false);
+  const [isSessionIdsLoading, setIsSessionIdsLoading] = useState(false);
+  const [availableSessionIds, setAvailableSessionIds] = useState<string[]>([]);
+  const debouncedSessionIdSearchTerm = useDebounce(localFilters.sessionId ?? "", 300);
+  const sessionIdSearchRequestIdRef = useRef(0);
+  const lastLoadedSessionIdSuggestionsKeyRef = useRef<string | undefined>(undefined);
 
   useEffect(() => {
     isMountedRef.current = true;
@@ -181,6 +194,84 @@ export function UsageLogsFilters({
     }
   }, [isAdmin, userPopoverOpen]);
 
+  const loadSessionIdsForFilter = useCallback(
+    async (term: string) => {
+      const requestId = ++sessionIdSearchRequestIdRef.current;
+      setIsSessionIdsLoading(true);
+      const requestKey = [
+        term,
+        isAdmin ? (localFilters.userId ?? "").toString() : "",
+        (localFilters.keyId ?? "").toString(),
+        (localFilters.providerId ?? "").toString(),
+        isAdmin ? "1" : "0",
+      ].join("|");
+      lastLoadedSessionIdSuggestionsKeyRef.current = requestKey;
+
+      try {
+        const result = await getUsageLogSessionIdSuggestions({
+          term,
+          userId: isAdmin ? localFilters.userId : undefined,
+          keyId: localFilters.keyId,
+          providerId: localFilters.providerId,
+        });
+
+        if (!isMountedRef.current || requestId !== sessionIdSearchRequestIdRef.current) return;
+
+        if (result.ok) {
+          setAvailableSessionIds(result.data);
+        } else {
+          console.error("Failed to load sessionId suggestions:", result.error);
+          setAvailableSessionIds([]);
+        }
+      } catch (error) {
+        if (!isMountedRef.current || requestId !== sessionIdSearchRequestIdRef.current) return;
+        console.error("Failed to load sessionId suggestions:", error);
+        setAvailableSessionIds([]);
+      } finally {
+        if (isMountedRef.current && requestId === sessionIdSearchRequestIdRef.current) {
+          setIsSessionIdsLoading(false);
+        }
+      }
+    },
+    [isAdmin, localFilters.keyId, localFilters.providerId, localFilters.userId]
+  );
+
+  useEffect(() => {
+    if (!sessionIdPopoverOpen) return;
+
+    const term = debouncedSessionIdSearchTerm.trim();
+    if (term.length < SESSION_ID_SUGGESTION_MIN_LEN) {
+      setAvailableSessionIds([]);
+      lastLoadedSessionIdSuggestionsKeyRef.current = undefined;
+      return;
+    }
+
+    const requestKey = [
+      term,
+      isAdmin ? (localFilters.userId ?? "").toString() : "",
+      (localFilters.keyId ?? "").toString(),
+      (localFilters.providerId ?? "").toString(),
+      isAdmin ? "1" : "0",
+    ].join("|");
+    if (requestKey === lastLoadedSessionIdSuggestionsKeyRef.current) return;
+    void loadSessionIdsForFilter(term);
+  }, [
+    sessionIdPopoverOpen,
+    debouncedSessionIdSearchTerm,
+    isAdmin,
+    localFilters.userId,
+    localFilters.keyId,
+    localFilters.providerId,
+    loadSessionIdsForFilter,
+  ]);
+
+  useEffect(() => {
+    if (!sessionIdPopoverOpen) {
+      setAvailableSessionIds([]);
+      lastLoadedSessionIdSuggestionsKeyRef.current = undefined;
+    }
+  }, [sessionIdPopoverOpen]);
+
   useEffect(() => {
     if (initialKeys.length > 0) {
       setKeys(initialKeys);
@@ -228,7 +319,33 @@ export function UsageLogsFilters({
   };
 
   const handleApply = () => {
-    onChange(localFilters);
+    const {
+      userId,
+      keyId,
+      providerId,
+      sessionId,
+      startTime,
+      endTime,
+      statusCode,
+      excludeStatusCode200,
+      model,
+      endpoint,
+      minRetryCount,
+    } = localFilters;
+
+    onChange({
+      userId,
+      keyId,
+      providerId,
+      sessionId,
+      startTime,
+      endTime,
+      statusCode,
+      excludeStatusCode200,
+      model,
+      endpoint,
+      minRetryCount,
+    });
   };
 
   const handleReset = () => {
@@ -272,24 +389,28 @@ export function UsageLogsFilters({
     return format(date, "yyyy-MM-dd");
   }, []);
 
-  // Helper: parse date string to timestamp (start of day in browser timezone)
-  const dateStringToTimestamp = useCallback((dateStr: string): number => {
-    const [year, month, day] = dateStr.split("-").map(Number);
-    return new Date(year, month - 1, day, 0, 0, 0, 0).getTime();
-  }, []);
-
   // Memoized startDate for display (from timestamp)
   const displayStartDate = useMemo(() => {
     if (!localFilters.startTime) return undefined;
     return timestampToDateString(localFilters.startTime);
   }, [localFilters.startTime, timestampToDateString]);
 
-  // Memoized endDate calculation: endTime is next day 00:00, subtract 1 day to show correct end date
+  const displayStartClock = useMemo(() => {
+    if (!localFilters.startTime) return undefined;
+    return formatClockFromTimestamp(localFilters.startTime);
+  }, [localFilters.startTime]);
+
+  // Memoized endDate calculation: endTime is exclusive, use endTime-1s to infer inclusive display end date
   const displayEndDate = useMemo(() => {
     if (!localFilters.endTime) return undefined;
-    // endTime is next day 00:00, so subtract 1 day to get actual end date
-    const actualEndDate = new Date(localFilters.endTime - 24 * 60 * 60 * 1000);
-    return format(actualEndDate, "yyyy-MM-dd");
+    const inclusiveEndTime = inclusiveEndTimestampFromExclusive(localFilters.endTime);
+    return format(new Date(inclusiveEndTime), "yyyy-MM-dd");
+  }, [localFilters.endTime]);
+
+  const displayEndClock = useMemo(() => {
+    if (!localFilters.endTime) return undefined;
+    const inclusiveEndTime = inclusiveEndTimestampFromExclusive(localFilters.endTime);
+    return formatClockFromTimestamp(inclusiveEndTime);
   }, [localFilters.endTime]);
 
   // Memoized callback for date range changes
@@ -297,20 +418,21 @@ export function UsageLogsFilters({
     (range: { startDate?: string; endDate?: string }) => {
       if (range.startDate && range.endDate) {
         // Convert to millisecond timestamps:
-        // startTime: start of selected start date (00:00:00.000 in browser timezone)
-        // endTime: start of day AFTER selected end date (for < comparison)
-        const startTimestamp = dateStringToTimestamp(range.startDate);
-        const endDate = parse(range.endDate, "yyyy-MM-dd", new Date());
-        const nextDay = addDays(endDate, 1);
-        const endTimestamp = new Date(
-          nextDay.getFullYear(),
-          nextDay.getMonth(),
-          nextDay.getDate(),
-          0,
-          0,
-          0,
-          0
-        ).getTime();
+        // startTime: startDate + startClock (default 00:00:00)
+        // endTime: endDate + endClock as exclusive upper bound (endClock default 23:59:59)
+        const startClock = displayStartClock ?? "00:00:00";
+        const endClock = displayEndClock ?? "23:59:59";
+        const startTimestamp = dateStringWithClockToTimestamp(range.startDate, startClock);
+        const endInclusiveTimestamp = dateStringWithClockToTimestamp(range.endDate, endClock);
+        if (startTimestamp === undefined || endInclusiveTimestamp === undefined) {
+          setLocalFilters((prev) => ({
+            ...prev,
+            startTime: undefined,
+            endTime: undefined,
+          }));
+          return;
+        }
+        const endTimestamp = endInclusiveTimestamp + 1000;
         setLocalFilters((prev) => ({
           ...prev,
           startTime: startTimestamp,
@@ -324,7 +446,7 @@ export function UsageLogsFilters({
         }));
       }
     },
-    [dateStringToTimestamp]
+    [displayEndClock, displayStartClock]
   );
 
   return (
@@ -338,6 +460,56 @@ export function UsageLogsFilters({
             endDate={displayEndDate}
             onDateRangeChange={handleDateRangeChange}
           />
+          <div className="grid grid-cols-1 gap-2 md:grid-cols-2">
+            <div className="space-y-1">
+              <Label className="text-xs text-muted-foreground">{t("logs.filters.startTime")}</Label>
+              <Input
+                type="time"
+                step={1}
+                value={displayStartClock ?? ""}
+                disabled={!displayStartDate}
+                onChange={(e) => {
+                  const nextClock = e.target.value || "00:00:00";
+                  setLocalFilters((prev) => {
+                    if (!prev.startTime) return prev;
+                    const dateStr = timestampToDateString(prev.startTime);
+                    const startTime = dateStringWithClockToTimestamp(dateStr, nextClock);
+                    if (startTime === undefined) return prev;
+                    return {
+                      ...prev,
+                      startTime,
+                    };
+                  });
+                }}
+              />
+            </div>
+            <div className="space-y-1">
+              <Label className="text-xs text-muted-foreground">{t("logs.filters.endTime")}</Label>
+              <Input
+                type="time"
+                step={1}
+                value={displayEndClock ?? ""}
+                disabled={!displayEndDate}
+                onChange={(e) => {
+                  const nextClock = e.target.value || "23:59:59";
+                  setLocalFilters((prev) => {
+                    if (!prev.endTime) return prev;
+                    const inclusiveEndTime = inclusiveEndTimestampFromExclusive(prev.endTime);
+                    const endDateStr = timestampToDateString(inclusiveEndTime);
+                    const endInclusiveTimestamp = dateStringWithClockToTimestamp(
+                      endDateStr,
+                      nextClock
+                    );
+                    if (endInclusiveTimestamp === undefined) return prev;
+                    return {
+                      ...prev,
+                      endTime: endInclusiveTimestamp + 1000,
+                    };
+                  });
+                }}
+              />
+            </div>
+          </div>
         </div>
 
         {/* 用户选择(仅 Admin) */}
@@ -532,6 +704,63 @@ export function UsageLogsFilters({
           </div>
         )}
 
+        {/* Session ID 联想 */}
+        <div className="space-y-2 lg:col-span-4">
+          <Label>{t("logs.filters.sessionId")}</Label>
+          <Popover open={sessionIdPopoverOpen} onOpenChange={setSessionIdPopoverOpen}>
+            <PopoverAnchor asChild>
+              <Input
+                value={localFilters.sessionId ?? ""}
+                placeholder={t("logs.filters.searchSessionId")}
+                onFocus={() => {
+                  const term = (localFilters.sessionId ?? "").trim();
+                  setSessionIdPopoverOpen(term.length >= SESSION_ID_SUGGESTION_MIN_LEN);
+                }}
+                onChange={(e) => {
+                  const next = e.target.value.trim();
+                  setLocalFilters((prev) => ({ ...prev, sessionId: next || undefined }));
+                  setSessionIdPopoverOpen(next.length >= SESSION_ID_SUGGESTION_MIN_LEN);
+                }}
+              />
+            </PopoverAnchor>
+            <PopoverContent
+              className="w-[320px] p-0"
+              align="start"
+              onOpenAutoFocus={(e) => e.preventDefault()}
+              onWheel={(e) => e.stopPropagation()}
+              onTouchMove={(e) => e.stopPropagation()}
+            >
+              <Command shouldFilter={false}>
+                <CommandList className="max-h-[250px] overflow-y-auto">
+                  <CommandEmpty>
+                    {isSessionIdsLoading
+                      ? t("logs.stats.loading")
+                      : t("logs.filters.noSessionFound")}
+                  </CommandEmpty>
+                  <CommandGroup>
+                    {availableSessionIds.map((sessionId) => (
+                      <CommandItem
+                        key={sessionId}
+                        value={sessionId}
+                        onSelect={() => {
+                          setLocalFilters((prev) => ({ ...prev, sessionId }));
+                          setSessionIdPopoverOpen(false);
+                        }}
+                        className="cursor-pointer"
+                      >
+                        <span className="flex-1 font-mono text-xs truncate">{sessionId}</span>
+                        {localFilters.sessionId === sessionId && (
+                          <Check className="h-4 w-4 text-primary" />
+                        )}
+                      </CommandItem>
+                    ))}
+                  </CommandGroup>
+                </CommandList>
+              </Command>
+            </PopoverContent>
+          </Popover>
+        </div>
+
         {/* 模型选择 */}
         <div className="space-y-2 lg:col-span-4">
           <Label>{t("logs.filters.model")}</Label>

+ 1 - 0
src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx

@@ -17,6 +17,7 @@ interface UsageLogsStatsPanelProps {
     userId?: number;
     keyId?: number;
     providerId?: number;
+    sessionId?: string;
     startTime?: number;
     endTime?: number;
     statusCode?: number;

+ 56 - 0
src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx

@@ -10,6 +10,15 @@ vi.mock("next-intl", () => ({
   useTranslations: () => (key: string) => key,
 }));
 
+const toastMocks = vi.hoisted(() => ({
+  success: vi.fn(),
+  error: vi.fn(),
+}));
+
+vi.mock("sonner", () => ({
+  toast: toastMocks,
+}));
+
 vi.mock("@/components/ui/tooltip", () => ({
   TooltipProvider: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
   Tooltip: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
@@ -181,4 +190,51 @@ describe("usage-logs-table multiplier badge", () => {
     });
     container.remove();
   });
+
+  test("copies sessionId on click and shows toast", async () => {
+    const writeText = vi.fn(async () => {});
+    Object.defineProperty(navigator, "clipboard", {
+      value: { writeText },
+      configurable: true,
+    });
+    Object.defineProperty(window, "isSecureContext", {
+      value: true,
+      configurable: true,
+    });
+
+    const container = document.createElement("div");
+    document.body.appendChild(container);
+
+    const root = createRoot(container);
+    await act(async () => {
+      root.render(
+        <UsageLogsTable
+          logs={[makeLog({ id: 1, sessionId: "session_test" })]}
+          total={1}
+          page={1}
+          pageSize={50}
+          onPageChange={() => {}}
+          isPending={false}
+        />
+      );
+    });
+
+    const sessionBtn = Array.from(container.querySelectorAll("button")).find((b) =>
+      (b.textContent ?? "").includes("session_test")
+    );
+    expect(sessionBtn).not.toBeUndefined();
+
+    await act(async () => {
+      sessionBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(writeText).toHaveBeenCalledWith("session_test");
+    expect(toastMocks.success).toHaveBeenCalledWith("actions.copied");
+
+    await act(async () => {
+      root.unmount();
+    });
+    container.remove();
+  });
 });

+ 42 - 2
src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx

@@ -1,7 +1,8 @@
 "use client";
 
 import { useTranslations } from "next-intl";
-import { useState } from "react";
+import { type MouseEvent, useCallback, useState } from "react";
+import { toast } from "sonner";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { RelativeTime } from "@/components/ui/relative-time";
@@ -15,6 +16,7 @@ import {
 } from "@/components/ui/table";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
 import { cn, formatTokenAmount } from "@/lib/utils";
+import { copyTextToClipboard } from "@/lib/utils/clipboard";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import { formatCurrency } from "@/lib/utils/currency";
 import {
@@ -62,6 +64,18 @@ export function UsageLogsTable({
     scrollToRedirect: boolean;
   }>({ logId: null, scrollToRedirect: false });
 
+  const handleCopySessionIdClick = useCallback(
+    (event: MouseEvent<HTMLButtonElement>) => {
+      const sessionId = event.currentTarget.dataset.sessionId;
+      if (!sessionId) return;
+
+      void copyTextToClipboard(sessionId).then((ok) => {
+        if (ok) toast.success(t("actions.copied"));
+      });
+    },
+    [t]
+  );
+
   return (
     <div className="space-y-4">
       <div className="rounded-md border overflow-x-auto">
@@ -71,6 +85,7 @@ export function UsageLogsTable({
               <TableHead>{t("logs.columns.time")}</TableHead>
               <TableHead>{t("logs.columns.user")}</TableHead>
               <TableHead>{t("logs.columns.key")}</TableHead>
+              <TableHead>{t("logs.columns.sessionId")}</TableHead>
               <TableHead>{t("logs.columns.provider")}</TableHead>
               <TableHead>{t("logs.columns.model")}</TableHead>
               <TableHead className="text-right">{t("logs.columns.tokens")}</TableHead>
@@ -83,7 +98,7 @@ export function UsageLogsTable({
           <TableBody>
             {logs.length === 0 ? (
               <TableRow>
-                <TableCell colSpan={10} className="text-center text-muted-foreground">
+                <TableCell colSpan={11} className="text-center text-muted-foreground">
                   {t("logs.table.noData")}
                 </TableCell>
               </TableRow>
@@ -129,6 +144,31 @@ export function UsageLogsTable({
                     </TableCell>
                     <TableCell>{log.userName}</TableCell>
                     <TableCell className="font-mono text-xs">{log.keyName}</TableCell>
+                    <TableCell className="font-mono text-xs w-[140px] max-w-[140px]">
+                      {log.sessionId ? (
+                        <TooltipProvider>
+                          <Tooltip delayDuration={300}>
+                            <TooltipTrigger asChild>
+                              <button
+                                type="button"
+                                className="w-full text-left truncate cursor-pointer hover:underline"
+                                data-session-id={log.sessionId}
+                                onClick={handleCopySessionIdClick}
+                              >
+                                {log.sessionId}
+                              </button>
+                            </TooltipTrigger>
+                            <TooltipContent side="bottom" align="start" className="max-w-[500px]">
+                              <p className="text-xs whitespace-normal break-words font-mono">
+                                {log.sessionId}
+                              </p>
+                            </TooltipContent>
+                          </Tooltip>
+                        </TooltipProvider>
+                      ) : (
+                        <span className="text-muted-foreground">-</span>
+                      )}
+                    </TableCell>
                     <TableCell className="text-left">
                       {isWarmupSkipped ? (
                         // Warmup 被跳过的请求显示“抢答/跳过”标记

+ 30 - 52
src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx

@@ -13,6 +13,7 @@ import type { CurrencyCode } from "@/lib/utils/currency";
 import type { Key } from "@/types/key";
 import type { ProviderDisplay } from "@/types/provider";
 import type { BillingModelSource, SystemSettings } from "@/types/system-config";
+import { buildLogsUrlQuery, parseLogsUrlFilters } from "../_utils/logs-query";
 import { UsageLogsFilters } from "./usage-logs-filters";
 import { UsageLogsStatsPanel } from "./usage-logs-stats-panel";
 import { VirtualizedLogsTable, type VirtualizedLogsTableFilters } from "./virtualized-logs-table";
@@ -91,40 +92,33 @@ function UsageLogsViewContent({
   const resolvedKeys = initialKeys ?? (keysResult?.ok && keysResult.data ? keysResult.data : []);
 
   // Parse filters from URL with stable reference
-  const filters = useMemo<VirtualizedLogsTableFilters & { page?: number }>(
-    () => ({
-      userId: searchParams.userId ? parseInt(searchParams.userId as string, 10) : undefined,
-      keyId: searchParams.keyId ? parseInt(searchParams.keyId as string, 10) : undefined,
-      providerId: searchParams.providerId
-        ? parseInt(searchParams.providerId as string, 10)
-        : undefined,
-      startTime: searchParams.startTime
-        ? parseInt(searchParams.startTime as string, 10)
-        : undefined,
-      endTime: searchParams.endTime ? parseInt(searchParams.endTime as string, 10) : undefined,
-      statusCode:
-        searchParams.statusCode && searchParams.statusCode !== "!200"
-          ? parseInt(searchParams.statusCode as string, 10)
-          : undefined,
-      excludeStatusCode200: searchParams.statusCode === "!200",
-      model: searchParams.model as string | undefined,
-      endpoint: searchParams.endpoint as string | undefined,
-      minRetryCount: searchParams.minRetry
-        ? parseInt(searchParams.minRetry as string, 10)
-        : undefined,
-    }),
-    [
-      searchParams.userId,
-      searchParams.keyId,
-      searchParams.providerId,
-      searchParams.startTime,
-      searchParams.endTime,
-      searchParams.statusCode,
-      searchParams.model,
-      searchParams.endpoint,
-      searchParams.minRetry,
-    ]
-  );
+  const filters = useMemo<VirtualizedLogsTableFilters & { page?: number }>(() => {
+    return parseLogsUrlFilters({
+      userId: searchParams.userId,
+      keyId: searchParams.keyId,
+      providerId: searchParams.providerId,
+      sessionId: searchParams.sessionId,
+      startTime: searchParams.startTime,
+      endTime: searchParams.endTime,
+      statusCode: searchParams.statusCode,
+      model: searchParams.model,
+      endpoint: searchParams.endpoint,
+      minRetry: searchParams.minRetry,
+      page: searchParams.page,
+    }) as VirtualizedLogsTableFilters & { page?: number };
+  }, [
+    searchParams.userId,
+    searchParams.keyId,
+    searchParams.providerId,
+    searchParams.sessionId,
+    searchParams.startTime,
+    searchParams.endTime,
+    searchParams.statusCode,
+    searchParams.model,
+    searchParams.endpoint,
+    searchParams.minRetry,
+    searchParams.page,
+  ]);
 
   // Manual refresh handler
   const handleManualRefresh = useCallback(async () => {
@@ -138,24 +132,7 @@ function UsageLogsViewContent({
 
   // Handle filter changes
   const handleFilterChange = (newFilters: Omit<typeof filters, "page">) => {
-    const query = new URLSearchParams();
-
-    if (newFilters.userId) query.set("userId", newFilters.userId.toString());
-    if (newFilters.keyId) query.set("keyId", newFilters.keyId.toString());
-    if (newFilters.providerId) query.set("providerId", newFilters.providerId.toString());
-    if (newFilters.startTime) query.set("startTime", newFilters.startTime.toString());
-    if (newFilters.endTime) query.set("endTime", newFilters.endTime.toString());
-    if (newFilters.excludeStatusCode200) {
-      query.set("statusCode", "!200");
-    } else if (newFilters.statusCode !== undefined) {
-      query.set("statusCode", newFilters.statusCode.toString());
-    }
-    if (newFilters.model) query.set("model", newFilters.model);
-    if (newFilters.endpoint) query.set("endpoint", newFilters.endpoint);
-    if (newFilters.minRetryCount !== undefined) {
-      query.set("minRetry", newFilters.minRetryCount.toString());
-    }
-
+    const query = buildLogsUrlQuery(newFilters);
     router.push(`/dashboard/logs?${query.toString()}`);
   };
 
@@ -181,6 +158,7 @@ function UsageLogsViewContent({
           userId: filters.userId,
           keyId: filters.keyId,
           providerId: filters.providerId,
+          sessionId: filters.sessionId,
           startTime: filters.startTime,
           endTime: filters.endTime,
           statusCode: filters.statusCode,

+ 5 - 52
src/app/[locale]/dashboard/logs/_components/usage-logs-view.tsx

@@ -13,6 +13,7 @@ import type { UsageLogsResult } from "@/repository/usage-logs";
 import type { Key } from "@/types/key";
 import type { ProviderDisplay } from "@/types/provider";
 import type { BillingModelSource } from "@/types/system-config";
+import { buildLogsUrlQuery, parseLogsUrlFilters } from "../_utils/logs-query";
 import { UsageLogsFilters } from "./usage-logs-filters";
 import { UsageLogsStatsPanel } from "./usage-logs-stats-panel";
 import { UsageLogsTable } from "./usage-logs-table";
@@ -50,39 +51,8 @@ export function UsageLogsView({
 
   // 从 URL 参数解析筛选条件
   // 使用毫秒时间戳传递时间,避免时区问题
-  const filters: {
-    userId?: number;
-    keyId?: number;
-    providerId?: number;
-    startTime?: number;
-    endTime?: number;
-    statusCode?: number;
-    excludeStatusCode200?: boolean;
-    model?: string;
-    endpoint?: string;
-    minRetryCount?: number;
-    page: number;
-  } = {
-    userId: searchParams.userId ? parseInt(searchParams.userId as string, 10) : undefined,
-    keyId: searchParams.keyId ? parseInt(searchParams.keyId as string, 10) : undefined,
-    providerId: searchParams.providerId
-      ? parseInt(searchParams.providerId as string, 10)
-      : undefined,
-    // 使用毫秒时间戳,无时区歧义
-    startTime: searchParams.startTime ? parseInt(searchParams.startTime as string, 10) : undefined,
-    endTime: searchParams.endTime ? parseInt(searchParams.endTime as string, 10) : undefined,
-    statusCode:
-      searchParams.statusCode && searchParams.statusCode !== "!200"
-        ? parseInt(searchParams.statusCode as string, 10)
-        : undefined,
-    excludeStatusCode200: searchParams.statusCode === "!200",
-    model: searchParams.model as string | undefined,
-    endpoint: searchParams.endpoint as string | undefined,
-    minRetryCount: searchParams.minRetry
-      ? parseInt(searchParams.minRetry as string, 10)
-      : undefined,
-    page: searchParams.page ? parseInt(searchParams.page as string, 10) : 1,
-  };
+  const parsedFilters = parseLogsUrlFilters(searchParams);
+  const filters = { ...parsedFilters, page: parsedFilters.page ?? 1 } as const;
 
   // 使用 ref 来存储最新的值,避免闭包陷阱
   const isPendingRef = useRef(isPending);
@@ -176,25 +146,7 @@ export function UsageLogsView({
 
   // 处理筛选条件变更
   const handleFilterChange = (newFilters: Omit<typeof filters, "page">) => {
-    const query = new URLSearchParams();
-
-    if (newFilters.userId) query.set("userId", newFilters.userId.toString());
-    if (newFilters.keyId) query.set("keyId", newFilters.keyId.toString());
-    if (newFilters.providerId) query.set("providerId", newFilters.providerId.toString());
-    // 使用毫秒时间戳传递时间,无时区歧义
-    if (newFilters.startTime) query.set("startTime", newFilters.startTime.toString());
-    if (newFilters.endTime) query.set("endTime", newFilters.endTime.toString());
-    if (newFilters.excludeStatusCode200) {
-      query.set("statusCode", "!200");
-    } else if (newFilters.statusCode !== undefined) {
-      query.set("statusCode", newFilters.statusCode.toString());
-    }
-    if (newFilters.model) query.set("model", newFilters.model);
-    if (newFilters.endpoint) query.set("endpoint", newFilters.endpoint);
-    if (newFilters.minRetryCount !== undefined) {
-      query.set("minRetry", newFilters.minRetryCount.toString());
-    }
-
+    const query = buildLogsUrlQuery(newFilters);
     router.push(`/dashboard/logs?${query.toString()}`);
   };
 
@@ -213,6 +165,7 @@ export function UsageLogsView({
           userId: filters.userId,
           keyId: filters.keyId,
           providerId: filters.providerId,
+          sessionId: filters.sessionId,
           startTime: filters.startTime,
           endTime: filters.endTime,
           statusCode: filters.statusCode,

+ 49 - 1
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx

@@ -3,7 +3,8 @@
 import { useInfiniteQuery } from "@tanstack/react-query";
 import { ArrowUp, Loader2 } from "lucide-react";
 import { useTranslations } from "next-intl";
-import { useCallback, useEffect, useRef, useState } from "react";
+import { type MouseEvent, useCallback, useEffect, useRef, useState } from "react";
+import { toast } from "sonner";
 import { getUsageLogsBatch } from "@/actions/usage-logs";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
@@ -11,6 +12,7 @@ import { RelativeTime } from "@/components/ui/relative-time";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
 import { useVirtualizer } from "@/hooks/use-virtualizer";
 import { cn, formatTokenAmount } from "@/lib/utils";
+import { copyTextToClipboard } from "@/lib/utils/clipboard";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import { formatCurrency } from "@/lib/utils/currency";
 import {
@@ -31,6 +33,7 @@ export interface VirtualizedLogsTableFilters {
   userId?: number;
   keyId?: number;
   providerId?: number;
+  sessionId?: string;
   startTime?: number;
   endTime?: number;
   statusCode?: number;
@@ -66,6 +69,18 @@ export function VirtualizedLogsTable({
     scrollToRedirect: boolean;
   }>({ logId: null, scrollToRedirect: false });
 
+  const handleCopySessionIdClick = useCallback(
+    (event: MouseEvent<HTMLButtonElement>) => {
+      const sessionId = event.currentTarget.dataset.sessionId;
+      if (!sessionId) return;
+
+      void copyTextToClipboard(sessionId).then((ok) => {
+        if (ok) toast.success(t("actions.copied"));
+      });
+    },
+    [t]
+  );
+
   // Infinite query with cursor-based pagination
   const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError, error } =
     useInfiniteQuery({
@@ -179,6 +194,12 @@ export function VirtualizedLogsTable({
             <div className="flex-[0.6] min-w-[50px] px-1 truncate" title={t("logs.columns.key")}>
               {t("logs.columns.key")}
             </div>
+            <div
+              className="flex-[0.8] min-w-[80px] px-1 truncate"
+              title={t("logs.columns.sessionId")}
+            >
+              {t("logs.columns.sessionId")}
+            </div>
             <div
               className="flex-[1.5] min-w-[100px] px-1 truncate"
               title={t("logs.columns.provider")}
@@ -286,6 +307,33 @@ export function VirtualizedLogsTable({
                     {log.keyName}
                   </div>
 
+                  {/* Session ID */}
+                  <div className="flex-[0.8] min-w-[80px] px-1">
+                    {log.sessionId ? (
+                      <TooltipProvider>
+                        <Tooltip delayDuration={300}>
+                          <TooltipTrigger asChild>
+                            <button
+                              type="button"
+                              className="w-full text-left font-mono text-xs truncate cursor-pointer hover:underline"
+                              data-session-id={log.sessionId}
+                              onClick={handleCopySessionIdClick}
+                            >
+                              {log.sessionId}
+                            </button>
+                          </TooltipTrigger>
+                          <TooltipContent side="bottom" align="start" className="max-w-[500px]">
+                            <p className="text-xs whitespace-normal break-words font-mono">
+                              {log.sessionId}
+                            </p>
+                          </TooltipContent>
+                        </Tooltip>
+                      </TooltipProvider>
+                    ) : (
+                      <span className="font-mono text-xs text-muted-foreground">-</span>
+                    )}
+                  </div>
+
                   {/* Provider */}
                   <div className="flex-[1.5] min-w-[100px] px-1">
                     {log.blockedBy ? (

+ 93 - 0
src/app/[locale]/dashboard/logs/_utils/logs-query.ts

@@ -0,0 +1,93 @@
+export interface LogsUrlFilters {
+  userId?: number;
+  keyId?: number;
+  providerId?: number;
+  sessionId?: string;
+  startTime?: number;
+  endTime?: number;
+  statusCode?: number;
+  excludeStatusCode200?: boolean;
+  model?: string;
+  endpoint?: string;
+  minRetryCount?: number;
+  page?: number;
+}
+
+function firstString(value: string | string[] | undefined): string | undefined {
+  if (Array.isArray(value)) return value[0];
+  return value;
+}
+
+function parseIntParam(value: string | string[] | undefined): number | undefined {
+  const raw = firstString(value);
+  if (!raw) return undefined;
+  const num = Number.parseInt(raw, 10);
+  return Number.isFinite(num) ? num : undefined;
+}
+
+function parseStringParam(value: string | string[] | undefined): string | undefined {
+  const raw = firstString(value);
+  const trimmed = raw?.trim();
+  return trimmed ? trimmed : undefined;
+}
+
+export function parseLogsUrlFilters(searchParams: {
+  [key: string]: string | string[] | undefined;
+}): LogsUrlFilters {
+  const statusCodeParam = parseStringParam(searchParams.statusCode);
+  const pageRaw = parseIntParam(searchParams.page);
+  const page = pageRaw && pageRaw >= 1 ? pageRaw : undefined;
+
+  const statusCode =
+    statusCodeParam && statusCodeParam !== "!200"
+      ? Number.parseInt(statusCodeParam, 10)
+      : undefined;
+
+  return {
+    userId: parseIntParam(searchParams.userId),
+    keyId: parseIntParam(searchParams.keyId),
+    providerId: parseIntParam(searchParams.providerId),
+    sessionId: parseStringParam(searchParams.sessionId),
+    startTime: parseIntParam(searchParams.startTime),
+    endTime: parseIntParam(searchParams.endTime),
+    statusCode: Number.isFinite(statusCode) ? statusCode : undefined,
+    excludeStatusCode200: statusCodeParam === "!200",
+    model: parseStringParam(searchParams.model),
+    endpoint: parseStringParam(searchParams.endpoint),
+    minRetryCount: parseIntParam(searchParams.minRetry),
+    page,
+  };
+}
+
+export function buildLogsUrlQuery(filters: LogsUrlFilters): URLSearchParams {
+  const query = new URLSearchParams();
+
+  if (filters.userId !== undefined) query.set("userId", filters.userId.toString());
+  if (filters.keyId !== undefined) query.set("keyId", filters.keyId.toString());
+  if (filters.providerId !== undefined) query.set("providerId", filters.providerId.toString());
+
+  const sessionId = filters.sessionId?.trim();
+  if (sessionId) query.set("sessionId", sessionId);
+
+  if (filters.startTime !== undefined) query.set("startTime", filters.startTime.toString());
+  if (filters.endTime !== undefined) query.set("endTime", filters.endTime.toString());
+
+  if (filters.excludeStatusCode200) {
+    query.set("statusCode", "!200");
+  } else if (filters.statusCode !== undefined) {
+    query.set("statusCode", filters.statusCode.toString());
+  }
+
+  if (filters.model) query.set("model", filters.model);
+  if (filters.endpoint) query.set("endpoint", filters.endpoint);
+
+  if (filters.minRetryCount !== undefined) {
+    query.set("minRetry", filters.minRetryCount.toString());
+  }
+
+  if (filters.page !== undefined && filters.page > 1) {
+    query.set("page", filters.page.toString());
+  }
+
+  return query;
+}

+ 49 - 0
src/app/[locale]/dashboard/logs/_utils/time-range.ts

@@ -0,0 +1,49 @@
+export interface ClockParts {
+  hours: number;
+  minutes: number;
+  seconds: number;
+}
+
+export function parseClockString(clockStr: string): ClockParts {
+  const [hoursRaw, minutesRaw, secondsRaw] = clockStr.split(":");
+
+  const hours = Number(hoursRaw);
+  const minutes = Number(minutesRaw);
+  const seconds = Number(secondsRaw ?? "0");
+
+  return {
+    hours: Number.isFinite(hours) ? hours : 0,
+    minutes: Number.isFinite(minutes) ? minutes : 0,
+    seconds: Number.isFinite(seconds) ? seconds : 0,
+  };
+}
+
+export function formatClockFromTimestamp(timestamp: number): string {
+  const date = new Date(timestamp);
+  const hh = `${date.getHours()}`.padStart(2, "0");
+  const mm = `${date.getMinutes()}`.padStart(2, "0");
+  const ss = `${date.getSeconds()}`.padStart(2, "0");
+  return `${hh}:${mm}:${ss}`;
+}
+
+export function dateStringWithClockToTimestamp(
+  dateStr: string,
+  clockStr: string
+): number | undefined {
+  const [year, month, day] = dateStr.split("-").map(Number);
+  const { hours, minutes, seconds } = parseClockString(clockStr);
+
+  const date = new Date(year, month - 1, day, hours, minutes, seconds, 0);
+  const timestamp = date.getTime();
+  if (!Number.isFinite(timestamp)) return undefined;
+
+  if (date.getFullYear() !== year) return undefined;
+  if (date.getMonth() !== month - 1) return undefined;
+  if (date.getDate() !== day) return undefined;
+
+  return timestamp;
+}
+
+export function inclusiveEndTimestampFromExclusive(endExclusiveTimestamp: number): number {
+  return Math.max(0, endExclusiveTimestamp - 1000);
+}

+ 17 - 11
src/app/v1/_lib/codex/chat-completions-handler.ts

@@ -10,13 +10,14 @@ import type { Context } from "hono";
 import { logger } from "@/lib/logger";
 import { ProxyStatusTracker } from "@/lib/proxy-status-tracker";
 import { SessionTracker } from "@/lib/session-tracker";
-import { ProxyErrorHandler } from "../proxy/error-handler";
-import { ProxyError } from "../proxy/errors";
-import { ProxyForwarder } from "../proxy/forwarder";
-import { GuardPipelineBuilder, RequestType } from "../proxy/guard-pipeline";
-import { ProxyResponseHandler } from "../proxy/response-handler";
-import { ProxyResponses } from "../proxy/responses";
-import { ProxySession } from "../proxy/session";
+import { ProxyErrorHandler } from "@/app/v1/_lib/proxy/error-handler";
+import { ProxyError } from "@/app/v1/_lib/proxy/errors";
+import { ProxyForwarder } from "@/app/v1/_lib/proxy/forwarder";
+import { GuardPipelineBuilder, RequestType } from "@/app/v1/_lib/proxy/guard-pipeline";
+import { attachSessionIdToErrorResponse } from "@/app/v1/_lib/proxy/error-session-id";
+import { ProxyResponseHandler } from "@/app/v1/_lib/proxy/response-handler";
+import { ProxyResponses } from "@/app/v1/_lib/proxy/responses";
+import { ProxySession } from "@/app/v1/_lib/proxy/session";
 import type { ChatCompletionRequest } from "./types/compatible";
 
 /**
@@ -45,7 +46,7 @@ export async function handleChatCompletions(c: Context): Promise<Response> {
     const isResponseAPIFormat = "input" in request && Array.isArray(request.input);
 
     if (!isOpenAIFormat && !isResponseAPIFormat) {
-      return new Response(
+      const response = new Response(
         JSON.stringify({
           error: {
             message:
@@ -56,6 +57,7 @@ export async function handleChatCompletions(c: Context): Promise<Response> {
         }),
         { status: 400, headers: { "Content-Type": "application/json" } }
       );
+      return await attachSessionIdToErrorResponse(session.sessionId, response);
     }
 
     if (isOpenAIFormat) {
@@ -144,7 +146,7 @@ export async function handleChatCompletions(c: Context): Promise<Response> {
 
       // 验证必需字段
       if (!request.model) {
-        return new Response(
+        const response = new Response(
           JSON.stringify({
             error: {
               message: "Invalid request: model is required",
@@ -154,6 +156,7 @@ export async function handleChatCompletions(c: Context): Promise<Response> {
           }),
           { status: 400, headers: { "Content-Type": "application/json" } }
         );
+        return await attachSessionIdToErrorResponse(session.sessionId, response);
       }
     }
 
@@ -161,7 +164,9 @@ export async function handleChatCompletions(c: Context): Promise<Response> {
     const pipeline = GuardPipelineBuilder.fromRequestType(type);
 
     const early = await pipeline.run(session);
-    if (early) return early;
+    if (early) {
+      return await attachSessionIdToErrorResponse(session.sessionId, early);
+    }
 
     // 增加并发计数(在所有检查通过后,请求开始前)- 跳过 count_tokens
     if (session.sessionId && !session.isCountTokensRequest()) {
@@ -187,7 +192,8 @@ export async function handleChatCompletions(c: Context): Promise<Response> {
     const response = await ProxyForwarder.send(session);
 
     // 5. 响应处理(自动转换回 OpenAI 格式)
-    return await ProxyResponseHandler.dispatch(session, response);
+    const handled = await ProxyResponseHandler.dispatch(session, response);
+    return await attachSessionIdToErrorResponse(session.sessionId, handled);
   } catch (error) {
     logger.error("[ChatCompletions] Handler error:", error);
     if (session) {

+ 6 - 2
src/app/v1/_lib/proxy-handler.ts

@@ -9,6 +9,7 @@ import { ProxyForwarder } from "./proxy/forwarder";
 import { GuardPipelineBuilder, RequestType } from "./proxy/guard-pipeline";
 import { ProxyResponseHandler } from "./proxy/response-handler";
 import { ProxyResponses } from "./proxy/responses";
+import { attachSessionIdToErrorResponse } from "./proxy/error-session-id";
 import { ProxySession } from "./proxy/session";
 
 export async function handleProxyRequest(c: Context): Promise<Response> {
@@ -54,7 +55,9 @@ export async function handleProxyRequest(c: Context): Promise<Response> {
 
     // Run guard chain; may return early Response
     const early = await pipeline.run(session);
-    if (early) return early;
+    if (early) {
+      return await attachSessionIdToErrorResponse(session.sessionId, early);
+    }
 
     // 9. 增加并发计数(在所有检查通过后,请求开始前)- 跳过 count_tokens
     if (session.sessionId && !session.isCountTokensRequest()) {
@@ -76,7 +79,8 @@ export async function handleProxyRequest(c: Context): Promise<Response> {
     }
 
     const response = await ProxyForwarder.send(session);
-    return await ProxyResponseHandler.dispatch(session, response);
+    const handled = await ProxyResponseHandler.dispatch(session, response);
+    return await attachSessionIdToErrorResponse(session.sessionId, handled);
   } catch (error) {
     logger.error("Proxy handler error:", error);
     if (session) {

+ 39 - 23
src/app/v1/_lib/proxy/error-handler.ts

@@ -14,6 +14,7 @@ import {
   ProxyError,
   type RateLimitError,
 } from "./errors";
+import { attachSessionIdToErrorResponse } from "./error-session-id";
 import { ProxyResponses } from "./responses";
 import type { ProxySession } from "./session";
 
@@ -60,7 +61,7 @@ export class ProxyErrorHandler {
         rateLimitMetadata
       );
 
-      return response;
+      return await attachSessionIdToErrorResponse(session.sessionId, response);
     }
 
     // 识别 ProxyError,提取详细信息(包含上游响应)
@@ -132,21 +133,27 @@ export class ProxyErrorHandler {
             });
             // 跳过响应体覆写,但仍可应用状态码覆写
             if (override.statusCode !== null) {
-              return ProxyResponses.buildError(
-                responseStatusCode,
+              return await attachSessionIdToErrorResponse(
+                session.sessionId,
+                ProxyResponses.buildError(
+                  responseStatusCode,
+                  clientErrorMessage,
+                  undefined,
+                  undefined,
+                  safeRequestId
+                )
+              );
+            }
+            // 两者都无效,返回原始错误(但仍透传 request_id,因为有覆写意图)
+            return await attachSessionIdToErrorResponse(
+              session.sessionId,
+              ProxyResponses.buildError(
+                statusCode,
                 clientErrorMessage,
                 undefined,
                 undefined,
                 safeRequestId
-              );
-            }
-            // 两者都无效,返回原始错误(但仍透传 request_id,因为有覆写意图)
-            return ProxyResponses.buildError(
-              statusCode,
-              clientErrorMessage,
-              undefined,
-              undefined,
-              safeRequestId
+              )
             );
           }
 
@@ -187,10 +194,13 @@ export class ProxyErrorHandler {
             overridden: true,
           });
 
-          return new Response(JSON.stringify(responseBody), {
-            status: responseStatusCode,
-            headers: { "Content-Type": "application/json" },
-          });
+          return await attachSessionIdToErrorResponse(
+            session.sessionId,
+            new Response(JSON.stringify(responseBody), {
+              status: responseStatusCode,
+              headers: { "Content-Type": "application/json" },
+            })
+          );
         }
 
         // 情况 2: 仅状态码覆写 - 返回客户端安全消息,但使用覆写的状态码
@@ -207,12 +217,15 @@ export class ProxyErrorHandler {
           overridden: true,
         });
 
-        return ProxyResponses.buildError(
-          responseStatusCode,
-          clientErrorMessage,
-          undefined,
-          undefined,
-          safeRequestId
+        return await attachSessionIdToErrorResponse(
+          session.sessionId,
+          ProxyResponses.buildError(
+            responseStatusCode,
+            clientErrorMessage,
+            undefined,
+            undefined,
+            safeRequestId
+          )
         );
       }
     }
@@ -223,7 +236,10 @@ export class ProxyErrorHandler {
       overridden: false,
     });
 
-    return ProxyResponses.buildError(statusCode, clientErrorMessage);
+    return await attachSessionIdToErrorResponse(
+      session.sessionId,
+      ProxyResponses.buildError(statusCode, clientErrorMessage)
+    );
   }
 
   /**

+ 56 - 0
src/app/v1/_lib/proxy/error-session-id.ts

@@ -0,0 +1,56 @@
+export function attachSessionIdToErrorMessage(
+  sessionId: string | null | undefined,
+  message: string
+): string {
+  if (!sessionId) return message;
+  if (message.includes("cch_session_id:")) return message;
+  return `${message} (cch_session_id: ${sessionId})`;
+}
+
+export async function attachSessionIdToErrorResponse(
+  sessionId: string | null | undefined,
+  response: Response
+): Promise<Response> {
+  if (!sessionId) return response;
+  if (response.status < 400) return response;
+
+  const headers = new Headers(response.headers);
+  headers.set("x-cch-session-id", sessionId);
+
+  const contentType = headers.get("content-type") || "";
+  if (contentType.includes("text/event-stream")) {
+    return new Response(response.body, { status: response.status, headers });
+  }
+
+  if (!contentType.includes("application/json")) {
+    return new Response(response.body, { status: response.status, headers });
+  }
+
+  let text: string;
+  try {
+    text = await response.clone().text();
+  } catch {
+    return new Response(response.body, { status: response.status, headers });
+  }
+
+  try {
+    const parsed = JSON.parse(text) as unknown;
+    if (
+      parsed &&
+      typeof parsed === "object" &&
+      "error" in parsed &&
+      parsed.error &&
+      typeof parsed.error === "object" &&
+      "message" in parsed.error &&
+      typeof (parsed.error as { message?: unknown }).message === "string"
+    ) {
+      const p = parsed as { error: { message: string } } & Record<string, unknown>;
+      p.error.message = attachSessionIdToErrorMessage(sessionId, p.error.message);
+      return new Response(JSON.stringify(p), { status: response.status, headers });
+    }
+  } catch {
+    // best-effort: keep original response body
+  }
+
+  return new Response(text, { status: response.status, headers });
+}

+ 2 - 0
src/drizzle/schema.ts

@@ -347,6 +347,8 @@ export const messageRequest = pgTable('message_request', {
   messageRequestUserQueryIdx: index('idx_message_request_user_query').on(table.userId, table.createdAt).where(sql`${table.deletedAt} IS NULL`),
   // Session 查询索引(按 session 聚合查看对话)
   messageRequestSessionIdIdx: index('idx_message_request_session_id').on(table.sessionId).where(sql`${table.deletedAt} IS NULL`),
+  // Session ID 前缀查询索引(LIKE 'prefix%',可稳定命中 B-tree)
+  messageRequestSessionIdPrefixIdx: index('idx_message_request_session_id_prefix').on(sql`${table.sessionId} varchar_pattern_ops`).where(sql`${table.deletedAt} IS NULL AND (${table.blockedBy} IS NULL OR ${table.blockedBy} <> 'warmup')`),
   // Session + Sequence 复合索引(用于 Session 内请求列表查询)
   messageRequestSessionSeqIdx: index('idx_message_request_session_seq').on(table.sessionId, table.requestSequence).where(sql`${table.deletedAt} IS NULL`),
   // Endpoint 过滤查询索引(仅针对未删除数据)

+ 3 - 0
src/lib/constants/usage-logs.constants.ts

@@ -0,0 +1,3 @@
+export const SESSION_ID_SUGGESTION_MIN_LEN = 2;
+export const SESSION_ID_SUGGESTION_MAX_LEN = 128;
+export const SESSION_ID_SUGGESTION_LIMIT = 20;

+ 39 - 11
src/lib/utils/clipboard.ts

@@ -11,20 +11,48 @@ export function isClipboardSupported(): boolean {
   return window.isSecureContext && !!navigator.clipboard?.writeText;
 }
 
+function tryCopyViaExecCommand(text: string): boolean {
+  if (typeof document === "undefined" || !document.body) return false;
+
+  try {
+    const textarea = document.createElement("textarea");
+    textarea.value = text;
+    textarea.setAttribute("readonly", "");
+    textarea.style.position = "absolute";
+    textarea.style.left = "-9999px";
+    document.body.appendChild(textarea);
+
+    textarea.select();
+
+    const ok = document.execCommand?.("copy") ?? false;
+    document.body.removeChild(textarea);
+    return ok;
+  } catch {
+    return false;
+  }
+}
+
 /**
- * 尝试复制文本到剪贴板
+ * 尝试复制文本到剪贴板(Clipboard API 优先,失败则走 execCommand fallback)
  * @returns 是否成功复制
  */
-export async function copyToClipboard(text: string): Promise<boolean> {
-  if (!isClipboardSupported()) {
-    return false;
-  }
+export async function copyTextToClipboard(text: string): Promise<boolean> {
+  if (typeof window === "undefined") return false;
 
-  try {
-    await navigator.clipboard.writeText(text);
-    return true;
-  } catch (err) {
-    console.error("复制失败:", err);
-    return false;
+  if (isClipboardSupported()) {
+    try {
+      await navigator.clipboard.writeText(text);
+      return true;
+    } catch {}
   }
+
+  return tryCopyViaExecCommand(text);
+}
+
+/**
+ * 尝试复制文本到剪贴板
+ * @returns 是否成功复制
+ */
+export async function copyToClipboard(text: string): Promise<boolean> {
+  return copyTextToClipboard(text);
 }

+ 3 - 0
src/repository/_shared/like.ts

@@ -0,0 +1,3 @@
+export function escapeLike(value: string): string {
+  return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
+}

+ 79 - 0
src/repository/usage-logs.ts

@@ -6,12 +6,15 @@ import { keys as keysTable, messageRequest, providers, users } from "@/drizzle/s
 import { buildUnifiedSpecialSettings } from "@/lib/utils/special-settings";
 import type { ProviderChainItem } from "@/types/message";
 import type { SpecialSetting } from "@/types/special-settings";
+import { escapeLike } from "./_shared/like";
 import { EXCLUDE_WARMUP_CONDITION } from "./_shared/message-request-conditions";
 
 export interface UsageLogFilters {
   userId?: number;
   keyId?: number;
   providerId?: number;
+  /** Session ID(精确匹配;空字符串/空白视为不筛选) */
+  sessionId?: string;
   /** 开始时间戳(毫秒),用于 >= 比较 */
   startTime?: number;
   /** 结束时间戳(毫秒),用于 < 比较 */
@@ -115,6 +118,7 @@ export async function findUsageLogsBatch(
     userId,
     keyId,
     providerId,
+    sessionId,
     startTime,
     endTime,
     statusCode,
@@ -141,6 +145,11 @@ export async function findUsageLogsBatch(
     conditions.push(eq(messageRequest.providerId, providerId));
   }
 
+  const trimmedSessionId = sessionId?.trim();
+  if (trimmedSessionId) {
+    conditions.push(eq(messageRequest.sessionId, trimmedSessionId));
+  }
+
   if (startTime !== undefined) {
     const startDate = new Date(startTime);
     conditions.push(sql`${messageRequest.createdAt} >= ${startDate.toISOString()}::timestamptz`);
@@ -320,6 +329,7 @@ export async function findUsageLogsWithDetails(filters: UsageLogFilters): Promis
     userId,
     keyId,
     providerId,
+    sessionId,
     startTime,
     endTime,
     statusCode,
@@ -346,6 +356,11 @@ export async function findUsageLogsWithDetails(filters: UsageLogFilters): Promis
     conditions.push(eq(messageRequest.providerId, providerId));
   }
 
+  const trimmedSessionId = sessionId?.trim();
+  if (trimmedSessionId) {
+    conditions.push(eq(messageRequest.sessionId, trimmedSessionId));
+  }
+
   // 使用毫秒时间戳进行时间比较
   // 前端传递的是浏览器本地时区的毫秒时间戳,直接与数据库的 timestamptz 比较
   // PostgreSQL 会自动处理时区转换
@@ -545,6 +560,64 @@ export async function getUsedEndpoints(): Promise<string[]> {
   return results.map((r) => r.endpoint).filter((e): e is string => e !== null);
 }
 
+export interface UsageLogSessionIdSuggestionFilters {
+  term: string;
+  userId?: number;
+  keyId?: number;
+  providerId?: number;
+  limit?: number;
+}
+
+export async function findUsageLogSessionIdSuggestions(
+  filters: UsageLogSessionIdSuggestionFilters
+): Promise<string[]> {
+  const { term, userId, keyId, providerId } = filters;
+  const limit = Math.min(50, Math.max(1, filters.limit ?? 20));
+  const trimmedTerm = term.trim();
+  if (!trimmedTerm) return [];
+
+  const pattern = `${escapeLike(trimmedTerm)}%`;
+  const conditions = [
+    isNull(messageRequest.deletedAt),
+    EXCLUDE_WARMUP_CONDITION,
+    sql`${messageRequest.sessionId} IS NOT NULL`,
+    sql`length(${messageRequest.sessionId}) > 0`,
+    sql`${messageRequest.sessionId} LIKE ${pattern} ESCAPE '\\'`,
+  ];
+
+  if (userId !== undefined) {
+    conditions.push(eq(messageRequest.userId, userId));
+  }
+
+  if (keyId !== undefined) {
+    conditions.push(eq(keysTable.id, keyId));
+  }
+
+  if (providerId !== undefined) {
+    conditions.push(eq(messageRequest.providerId, providerId));
+  }
+
+  const baseQuery = db
+    .select({
+      sessionId: messageRequest.sessionId,
+      firstSeen: sql<Date>`min(${messageRequest.createdAt})`,
+    })
+    .from(messageRequest);
+
+  const query =
+    keyId !== undefined
+      ? baseQuery.innerJoin(keysTable, eq(messageRequest.key, keysTable.key))
+      : baseQuery;
+
+  const results = await query
+    .where(and(...conditions))
+    .groupBy(messageRequest.sessionId)
+    .orderBy(desc(sql`min(${messageRequest.createdAt})`))
+    .limit(limit);
+
+  return results.map((r) => r.sessionId).filter((id): id is string => Boolean(id));
+}
+
 /**
  * 独立获取使用日志聚合统计(用于可折叠面板按需加载)
  *
@@ -560,6 +633,7 @@ export async function findUsageLogsStats(
     userId,
     keyId,
     providerId,
+    sessionId,
     startTime,
     endTime,
     statusCode,
@@ -584,6 +658,11 @@ export async function findUsageLogsStats(
     conditions.push(eq(messageRequest.providerId, providerId));
   }
 
+  const trimmedSessionId = sessionId?.trim();
+  if (trimmedSessionId) {
+    conditions.push(eq(messageRequest.sessionId, trimmedSessionId));
+  }
+
   if (startTime !== undefined) {
     const startDate = new Date(startTime);
     conditions.push(sql`${messageRequest.createdAt} >= ${startDate.toISOString()}::timestamptz`);

+ 128 - 0
tests/unit/dashboard-logs-filters-time-range.test.tsx

@@ -0,0 +1,128 @@
+/**
+ * @vitest-environment happy-dom
+ */
+
+import type { ReactNode } from "react";
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { NextIntlClientProvider } from "next-intl";
+import { describe, expect, test, vi } from "vitest";
+import { UsageLogsFilters } from "@/app/[locale]/dashboard/logs/_components/usage-logs-filters";
+import dashboardMessages from "../../messages/en/dashboard.json";
+
+vi.mock("@/app/[locale]/dashboard/logs/_components/logs-date-range-picker", () => ({
+  LogsDateRangePicker: ({
+    onDateRangeChange,
+  }: {
+    onDateRangeChange: (range: { startDate?: string; endDate?: string }) => void;
+  }) => (
+    <button
+      type="button"
+      data-testid="mock-date-range"
+      onClick={() => onDateRangeChange({ startDate: "2026-01-01", endDate: "2026-01-02" })}
+    >
+      Mock Date Range
+    </button>
+  ),
+}));
+
+function renderWithIntl(node: ReactNode) {
+  const container = document.createElement("div");
+  document.body.appendChild(container);
+  const root = createRoot(container);
+
+  act(() => {
+    root.render(
+      <NextIntlClientProvider
+        locale="en"
+        messages={{ dashboard: dashboardMessages }}
+        timeZone="UTC"
+      >
+        {node}
+      </NextIntlClientProvider>
+    );
+  });
+
+  return {
+    container,
+    unmount: () => {
+      act(() => root.unmount());
+      container.remove();
+    },
+  };
+}
+
+async function actClick(el: Element | null) {
+  if (!el) throw new Error("element not found");
+  await act(async () => {
+    el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+  });
+}
+
+describe("UsageLogsFilters - seconds-level time range", () => {
+  test("defaults to full-day semantics (end is exclusive next-day 00:00:00)", async () => {
+    const onChange = vi.fn();
+
+    const { container, unmount } = renderWithIntl(
+      <UsageLogsFilters
+        isAdmin={false}
+        providers={[]}
+        initialKeys={[]}
+        filters={{}}
+        onChange={onChange}
+        onReset={() => {}}
+      />
+    );
+
+    await actClick(container.querySelector("[data-testid='mock-date-range']"));
+
+    const timeInputs = Array.from(container.querySelectorAll("input[type='time']"));
+    expect(timeInputs).toHaveLength(2);
+
+    const applyBtn = Array.from(container.querySelectorAll("button")).find(
+      (b) => (b.textContent || "").trim() === "Apply Filter"
+    );
+    await actClick(applyBtn ?? null);
+
+    const expectedStart = new Date(2026, 0, 1, 0, 0, 0, 0).getTime();
+    const expectedEnd = new Date(2026, 0, 3, 0, 0, 0, 0).getTime();
+
+    expect(onChange).toHaveBeenCalledWith(
+      expect.objectContaining({ startTime: expectedStart, endTime: expectedEnd })
+    );
+
+    unmount();
+  });
+
+  test("Apply drops leaked page field from runtime filters object", async () => {
+    const onChange = vi.fn();
+
+    const leakedFilters = { sessionId: "abc", page: 3 } as unknown as Parameters<
+      typeof UsageLogsFilters
+    >[0]["filters"];
+
+    const { container, unmount } = renderWithIntl(
+      <UsageLogsFilters
+        isAdmin={false}
+        providers={[]}
+        initialKeys={[]}
+        // Runtime filters object may carry extra fields (e.g. page) even if TS types omit them
+        filters={leakedFilters}
+        onChange={onChange}
+        onReset={() => {}}
+      />
+    );
+
+    const applyBtn = Array.from(container.querySelectorAll("button")).find(
+      (b) => (b.textContent || "").trim() === "Apply Filter"
+    );
+    await actClick(applyBtn ?? null);
+
+    expect(onChange).toHaveBeenCalledTimes(1);
+    const calledFilters = onChange.mock.calls[0]?.[0] as Record<string, unknown> | undefined;
+    expect(calledFilters).toEqual(expect.objectContaining({ sessionId: "abc" }));
+    expect(calledFilters && "page" in calledFilters).toBe(false);
+
+    unmount();
+  });
+});

+ 90 - 0
tests/unit/dashboard-logs-query-utils.test.ts

@@ -0,0 +1,90 @@
+import { describe, expect, test } from "vitest";
+import {
+  buildLogsUrlQuery,
+  parseLogsUrlFilters,
+} from "@/app/[locale]/dashboard/logs/_utils/logs-query";
+
+describe("dashboard logs url query utils", () => {
+  test("parses and trims sessionId", () => {
+    const parsed = parseLogsUrlFilters({ sessionId: "  abc  " });
+    expect(parsed.sessionId).toBe("abc");
+  });
+
+  test("array params use the first value", () => {
+    const parsed = parseLogsUrlFilters({
+      sessionId: ["  abc  ", "ignored"],
+      userId: ["1", "2"],
+      statusCode: ["!200", "200"],
+    });
+    expect(parsed.sessionId).toBe("abc");
+    expect(parsed.userId).toBe(1);
+    expect(parsed.excludeStatusCode200).toBe(true);
+  });
+
+  test("statusCode '!200' maps to excludeStatusCode200", () => {
+    const parsed = parseLogsUrlFilters({ statusCode: "!200" });
+    expect(parsed.excludeStatusCode200).toBe(true);
+    expect(parsed.statusCode).toBeUndefined();
+  });
+
+  test("parseIntParam returns undefined for invalid numbers", () => {
+    const parsed = parseLogsUrlFilters({ userId: "NaN", startTime: "bad" });
+    expect(parsed.userId).toBeUndefined();
+    expect(parsed.startTime).toBeUndefined();
+  });
+
+  test("buildLogsUrlQuery omits empty sessionId", () => {
+    const query = buildLogsUrlQuery({ sessionId: "   " });
+    expect(query.get("sessionId")).toBeNull();
+  });
+
+  test("buildLogsUrlQuery includes sessionId and time range", () => {
+    const query = buildLogsUrlQuery({ sessionId: "abc", startTime: 1, endTime: 2 });
+    expect(query.get("sessionId")).toBe("abc");
+    expect(query.get("startTime")).toBe("1");
+    expect(query.get("endTime")).toBe("2");
+  });
+
+  test("buildLogsUrlQuery includes startTime/endTime even when 0", () => {
+    const query = buildLogsUrlQuery({ startTime: 0, endTime: 0 });
+    expect(query.get("startTime")).toBe("0");
+    expect(query.get("endTime")).toBe("0");
+  });
+
+  test("parseLogsUrlFilters sanitizes invalid page (<1) to undefined", () => {
+    expect(parseLogsUrlFilters({ page: "0" }).page).toBeUndefined();
+    expect(parseLogsUrlFilters({ page: "-1" }).page).toBeUndefined();
+    expect(parseLogsUrlFilters({ page: "1" }).page).toBe(1);
+  });
+
+  test("buildLogsUrlQuery only includes page when > 1", () => {
+    expect(buildLogsUrlQuery({ page: 0 }).get("page")).toBeNull();
+    expect(buildLogsUrlQuery({ page: 1 }).get("page")).toBeNull();
+    expect(buildLogsUrlQuery({ page: 2 }).get("page")).toBe("2");
+  });
+
+  test("build + parse roundtrip preserves filters", () => {
+    const original = {
+      userId: 1,
+      keyId: 2,
+      providerId: 3,
+      sessionId: "abc",
+      startTime: 10,
+      endTime: 20,
+      statusCode: 500,
+      excludeStatusCode200: false,
+      model: "m",
+      endpoint: "/v1/messages",
+      minRetryCount: 2,
+    };
+    const query = buildLogsUrlQuery(original);
+
+    const parsed = parseLogsUrlFilters(Object.fromEntries(query.entries()));
+    expect(parsed).toEqual(expect.objectContaining(original));
+  });
+
+  test("buildLogsUrlQuery includes minRetryCount even when 0", () => {
+    const query = buildLogsUrlQuery({ minRetryCount: 0 });
+    expect(query.get("minRetry")).toBe("0");
+  });
+});

+ 322 - 0
tests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsx

@@ -0,0 +1,322 @@
+/**
+ * @vitest-environment happy-dom
+ */
+
+import type { ReactNode } from "react";
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { describe, expect, test, vi } from "vitest";
+import { UsageLogsFilters } from "@/app/[locale]/dashboard/logs/_components/usage-logs-filters";
+
+vi.mock("next-intl", () => ({
+  useTranslations: () => (key: string) => key,
+}));
+
+const toastMocks = vi.hoisted(() => ({
+  success: vi.fn(),
+  error: vi.fn(),
+}));
+
+vi.mock("sonner", () => ({
+  toast: toastMocks,
+}));
+
+const usageLogsActionMocks = vi.hoisted(() => ({
+  exportUsageLogs: vi.fn(async () => ({ ok: true, data: "" })),
+  getUsageLogSessionIdSuggestions: vi.fn(async () => ({ ok: true, data: ["session_1"] })),
+  getModelList: vi.fn(async () => ({ ok: true, data: [] })),
+  getStatusCodeList: vi.fn(async () => ({ ok: true, data: [] })),
+  getEndpointList: vi.fn(async () => ({ ok: true, data: [] })),
+}));
+
+const usersActionMocks = vi.hoisted(() => ({
+  searchUsersForFilter: vi.fn(async () => ({
+    ok: true,
+    data: [] as Array<{ id: number; name: string }>,
+  })),
+}));
+
+vi.mock("@/actions/usage-logs", () => ({
+  exportUsageLogs: usageLogsActionMocks.exportUsageLogs,
+  getUsageLogSessionIdSuggestions: usageLogsActionMocks.getUsageLogSessionIdSuggestions,
+  getModelList: usageLogsActionMocks.getModelList,
+  getStatusCodeList: usageLogsActionMocks.getStatusCodeList,
+  getEndpointList: usageLogsActionMocks.getEndpointList,
+}));
+
+vi.mock("@/actions/users", () => ({
+  searchUsersForFilter: usersActionMocks.searchUsersForFilter,
+}));
+
+vi.mock("@/components/ui/popover", async () => {
+  const React = await import("react");
+
+  type PopoverCtx = { open: boolean; onOpenChange?: (open: boolean) => void };
+  const PopoverContext = React.createContext<PopoverCtx>({ open: false });
+
+  function Popover({
+    open,
+    onOpenChange,
+    children,
+  }: {
+    open?: boolean;
+    onOpenChange?: (open: boolean) => void;
+    children?: ReactNode;
+  }) {
+    return (
+      <PopoverContext.Provider value={{ open: Boolean(open), onOpenChange }}>
+        {children}
+      </PopoverContext.Provider>
+    );
+  }
+
+  function PopoverTrigger({ asChild, children }: { asChild?: boolean; children?: ReactNode }) {
+    const { open, onOpenChange } = React.useContext(PopoverContext);
+    const child = React.Children.only(children) as unknown as {
+      props: { onClick?: (e: unknown) => void };
+    };
+
+    const handleClick = (e: unknown) => {
+      child.props.onClick?.(e);
+      onOpenChange?.(!open);
+    };
+
+    if (asChild) {
+      return React.cloneElement(child as never, { onClick: handleClick });
+    }
+
+    return (
+      <button type="button" onClick={handleClick}>
+        {children}
+      </button>
+    );
+  }
+
+  function PopoverContent({ children }: { children?: ReactNode }) {
+    const { open } = React.useContext(PopoverContext);
+    if (!open) return null;
+    return <div>{children}</div>;
+  }
+
+  function PopoverAnchor({ children }: { children?: ReactNode }) {
+    return <>{children}</>;
+  }
+
+  return {
+    Popover,
+    PopoverTrigger,
+    PopoverContent,
+    PopoverAnchor,
+  };
+});
+
+vi.mock("@/components/ui/tooltip", () => ({
+  TooltipProvider: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
+  Tooltip: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
+  TooltipTrigger: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
+  TooltipContent: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
+}));
+
+async function flushMicrotasks() {
+  await act(async () => {
+    await Promise.resolve();
+  });
+}
+
+async function actClick(el: Element | null) {
+  if (!el) throw new Error("element not found");
+  await act(async () => {
+    el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+  });
+}
+
+function setReactInputValue(input: HTMLInputElement, value: string) {
+  const prototype = Object.getPrototypeOf(input) as HTMLInputElement;
+  const descriptor = Object.getOwnPropertyDescriptor(prototype, "value");
+  descriptor?.set?.call(input, value);
+  input.dispatchEvent(new Event("input", { bubbles: true }));
+  input.dispatchEvent(new Event("change", { bubbles: true }));
+}
+
+describe("UsageLogsFilters sessionId suggestions", () => {
+  test("should debounce and require min length (>=2)", async () => {
+    vi.useFakeTimers();
+    vi.clearAllMocks();
+    document.body.innerHTML = "";
+
+    const container = document.createElement("div");
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(
+        <UsageLogsFilters
+          isAdmin={false}
+          providers={[]}
+          initialKeys={[]}
+          filters={{}}
+          onChange={() => {}}
+          onReset={() => {}}
+        />
+      );
+    });
+
+    const input = container.querySelector(
+      'input[placeholder="logs.filters.searchSessionId"]'
+    ) as HTMLInputElement | null;
+    expect(input).toBeTruthy();
+
+    await act(async () => {
+      setReactInputValue(input!, "a");
+    });
+
+    await act(async () => {
+      vi.advanceTimersByTime(350);
+    });
+    await flushMicrotasks();
+
+    expect(usageLogsActionMocks.getUsageLogSessionIdSuggestions).not.toHaveBeenCalled();
+
+    await act(async () => {
+      setReactInputValue(input!, "ab");
+    });
+
+    await act(async () => {
+      vi.advanceTimersByTime(299);
+    });
+    await flushMicrotasks();
+    expect(usageLogsActionMocks.getUsageLogSessionIdSuggestions).not.toHaveBeenCalled();
+
+    await act(async () => {
+      vi.advanceTimersByTime(1);
+    });
+    await flushMicrotasks();
+
+    expect(usageLogsActionMocks.getUsageLogSessionIdSuggestions).toHaveBeenCalledTimes(1);
+    expect(usageLogsActionMocks.getUsageLogSessionIdSuggestions).toHaveBeenCalledWith(
+      expect.objectContaining({ term: "ab" })
+    );
+
+    await act(async () => {
+      root.unmount();
+    });
+    container.remove();
+    vi.useRealTimers();
+  });
+
+  test("should keep input focused when opening suggestions popover", async () => {
+    vi.useFakeTimers();
+    vi.clearAllMocks();
+    document.body.innerHTML = "";
+
+    const container = document.createElement("div");
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(
+        <UsageLogsFilters
+          isAdmin={false}
+          providers={[]}
+          initialKeys={[]}
+          filters={{ sessionId: "ab" }}
+          onChange={() => {}}
+          onReset={() => {}}
+        />
+      );
+    });
+
+    const input = container.querySelector(
+      'input[placeholder="logs.filters.searchSessionId"]'
+    ) as HTMLInputElement | null;
+    expect(input).toBeTruthy();
+
+    await act(async () => {
+      input?.focus();
+    });
+    await flushMicrotasks();
+
+    expect(document.activeElement).toBe(input);
+
+    await act(async () => {
+      vi.advanceTimersByTime(350);
+    });
+    await flushMicrotasks();
+
+    expect(usageLogsActionMocks.getUsageLogSessionIdSuggestions).toHaveBeenCalledTimes(1);
+    expect(document.activeElement).toBe(input);
+
+    await act(async () => {
+      root.unmount();
+    });
+    container.remove();
+    vi.useRealTimers();
+  });
+
+  test("should reload suggestions when provider scope changes (term unchanged)", async () => {
+    vi.useFakeTimers();
+    vi.clearAllMocks();
+    document.body.innerHTML = "";
+
+    const container = document.createElement("div");
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(
+        <UsageLogsFilters
+          isAdmin={true}
+          providers={[
+            { id: 1, name: "p1" },
+            { id: 2, name: "p2" },
+          ]}
+          initialKeys={[]}
+          filters={{ sessionId: "ab" }}
+          onChange={() => {}}
+          onReset={() => {}}
+        />
+      );
+    });
+    await flushMicrotasks();
+
+    const input = container.querySelector(
+      'input[placeholder="logs.filters.searchSessionId"]'
+    ) as HTMLInputElement | null;
+    expect(input).toBeTruthy();
+
+    await act(async () => {
+      input?.focus();
+    });
+    await flushMicrotasks();
+
+    await act(async () => {
+      vi.advanceTimersByTime(350);
+    });
+    await flushMicrotasks();
+
+    expect(usageLogsActionMocks.getUsageLogSessionIdSuggestions).toHaveBeenCalledTimes(1);
+
+    const providerBtn = Array.from(container.querySelectorAll("button")).find((b) =>
+      (b.textContent || "").includes("logs.filters.allProviders")
+    );
+    await actClick(providerBtn ?? null);
+    await flushMicrotasks();
+
+    const providerItem = Array.from(document.querySelectorAll("[cmdk-item]")).find((el) =>
+      (el.textContent || "").includes("p1")
+    );
+    await actClick(providerItem ?? null);
+    await flushMicrotasks();
+
+    expect(usageLogsActionMocks.getUsageLogSessionIdSuggestions).toHaveBeenCalledTimes(2);
+    expect(usageLogsActionMocks.getUsageLogSessionIdSuggestions).toHaveBeenLastCalledWith(
+      expect.objectContaining({ term: "ab", providerId: 1 })
+    );
+
+    await act(async () => {
+      root.unmount();
+    });
+    container.remove();
+    vi.useRealTimers();
+  });
+});

+ 46 - 0
tests/unit/dashboard-logs-time-range-utils.test.ts

@@ -0,0 +1,46 @@
+import { describe, expect, test } from "vitest";
+import {
+  dateStringWithClockToTimestamp,
+  formatClockFromTimestamp,
+  inclusiveEndTimestampFromExclusive,
+  parseClockString,
+} from "@/app/[locale]/dashboard/logs/_utils/time-range";
+
+describe("dashboard logs time range utils", () => {
+  test("parseClockString supports HH:MM and defaults seconds to 0", () => {
+    expect(parseClockString("01:02")).toEqual({ hours: 1, minutes: 2, seconds: 0 });
+  });
+
+  test("parseClockString falls back to 0 for invalid numbers", () => {
+    expect(parseClockString("xx:yy:zz")).toEqual({ hours: 0, minutes: 0, seconds: 0 });
+    expect(parseClockString("01:02:xx")).toEqual({ hours: 1, minutes: 2, seconds: 0 });
+  });
+
+  test("dateStringWithClockToTimestamp combines local date + clock", () => {
+    const ts = dateStringWithClockToTimestamp("2026-01-01", "01:02:03");
+    const expected = new Date(2026, 0, 1, 1, 2, 3, 0).getTime();
+    expect(ts).toBe(expected);
+  });
+
+  test("dateStringWithClockToTimestamp returns undefined for invalid date", () => {
+    expect(dateStringWithClockToTimestamp("not-a-date", "01:02:03")).toBeUndefined();
+    expect(dateStringWithClockToTimestamp("2026-13-40", "01:02:03")).toBeUndefined();
+  });
+
+  test("exclusive end time round-trips to inclusive end time (+/-1s)", () => {
+    const inclusive = dateStringWithClockToTimestamp("2026-01-02", "04:05:06");
+    expect(inclusive).toBeDefined();
+    const exclusive = inclusive! + 1000;
+    expect(inclusiveEndTimestampFromExclusive(exclusive)).toBe(inclusive);
+  });
+
+  test("inclusiveEndTimestampFromExclusive clamps at 0", () => {
+    expect(inclusiveEndTimestampFromExclusive(0)).toBe(0);
+    expect(inclusiveEndTimestampFromExclusive(500)).toBe(0);
+  });
+
+  test("formatClockFromTimestamp uses HH:MM:SS", () => {
+    const ts = new Date(2026, 0, 1, 1, 2, 3, 0).getTime();
+    expect(formatClockFromTimestamp(ts)).toBe("01:02:03");
+  });
+});

+ 15 - 0
tests/unit/lib/constants/usage-logs.constants.test.ts

@@ -0,0 +1,15 @@
+import { describe, expect, test } from "vitest";
+
+import {
+  SESSION_ID_SUGGESTION_LIMIT,
+  SESSION_ID_SUGGESTION_MAX_LEN,
+  SESSION_ID_SUGGESTION_MIN_LEN,
+} from "@/lib/constants/usage-logs.constants";
+
+describe("Usage logs constants", () => {
+  test("SESSION_ID_SUGGESTION_* 常量保持稳定(避免前后端阈值漂移)", () => {
+    expect(SESSION_ID_SUGGESTION_MIN_LEN).toBe(2);
+    expect(SESSION_ID_SUGGESTION_MAX_LEN).toBe(128);
+    expect(SESSION_ID_SUGGESTION_LIMIT).toBe(20);
+  });
+});

+ 119 - 0
tests/unit/lib/utils/clipboard.test.ts

@@ -0,0 +1,119 @@
+import { afterEach, describe, expect, test, vi } from "vitest";
+
+import { copyTextToClipboard, copyToClipboard, isClipboardSupported } from "@/lib/utils/clipboard";
+
+function stubSecureContext(value: boolean) {
+  Object.defineProperty(window, "isSecureContext", {
+    value,
+    configurable: true,
+  });
+}
+
+function stubClipboard(writeText: (text: string) => Promise<void> | void) {
+  Object.defineProperty(navigator, "clipboard", {
+    value: { writeText },
+    configurable: true,
+  });
+}
+
+function stubExecCommand(impl: (command: string) => boolean) {
+  Object.defineProperty(document, "execCommand", {
+    value: impl,
+    configurable: true,
+  });
+}
+
+afterEach(() => {
+  vi.unstubAllGlobals();
+  vi.restoreAllMocks();
+});
+
+describe("clipboard utils", () => {
+  test("SSR 环境:isClipboardSupported/copyTextToClipboard 应返回 false", async () => {
+    vi.stubGlobal("window", undefined as unknown as Window);
+
+    expect(isClipboardSupported()).toBe(false);
+    await expect(copyTextToClipboard("abc")).resolves.toBe(false);
+  });
+
+  test("isClipboardSupported: 仅在安全上下文且 Clipboard API 可用时为 true", () => {
+    stubSecureContext(false);
+    stubClipboard(vi.fn());
+    expect(isClipboardSupported()).toBe(false);
+
+    stubSecureContext(true);
+    stubClipboard(vi.fn());
+    expect(isClipboardSupported()).toBe(true);
+  });
+
+  test("copyTextToClipboard: Clipboard API 成功时返回 true", async () => {
+    stubSecureContext(true);
+    const writeText = vi.fn().mockResolvedValue(undefined);
+    stubClipboard(writeText);
+
+    const execCommand = vi.fn();
+    stubExecCommand(execCommand);
+
+    const before = document.querySelectorAll("textarea").length;
+    await expect(copyTextToClipboard("abc")).resolves.toBe(true);
+    const after = document.querySelectorAll("textarea").length;
+
+    expect(writeText).toHaveBeenCalledWith("abc");
+    expect(execCommand).not.toHaveBeenCalled();
+    expect(after).toBe(before);
+  });
+
+  test("copyTextToClipboard: Clipboard API 失败时应 fallback 到 execCommand", async () => {
+    stubSecureContext(true);
+    const writeText = vi.fn().mockRejectedValue(new Error("fail"));
+    stubClipboard(writeText);
+
+    const execCommand = vi.fn(() => true);
+    stubExecCommand(execCommand);
+
+    const before = document.querySelectorAll("textarea").length;
+    await expect(copyTextToClipboard("abc")).resolves.toBe(true);
+    const after = document.querySelectorAll("textarea").length;
+
+    expect(writeText).toHaveBeenCalledWith("abc");
+    expect(execCommand).toHaveBeenCalledWith("copy");
+    expect(after).toBe(before);
+  });
+
+  test("copyTextToClipboard: 无 Clipboard API 时走 fallback(execCommand 失败则返回 false)", async () => {
+    stubSecureContext(false);
+    Object.defineProperty(navigator, "clipboard", { value: undefined, configurable: true });
+
+    stubExecCommand(() => false);
+
+    await expect(copyTextToClipboard("abc")).resolves.toBe(false);
+  });
+
+  test("copyTextToClipboard: fallback 抛错时返回 false", async () => {
+    stubSecureContext(false);
+    Object.defineProperty(navigator, "clipboard", { value: undefined, configurable: true });
+
+    stubExecCommand(() => {
+      throw new Error("boom");
+    });
+
+    await expect(copyTextToClipboard("abc")).resolves.toBe(false);
+  });
+
+  test("copyToClipboard: 兼容旧 API(内部调用 copyTextToClipboard)", async () => {
+    stubSecureContext(true);
+    const writeText = vi.fn().mockResolvedValue(undefined);
+    stubClipboard(writeText);
+
+    await expect(copyToClipboard("abc")).resolves.toBe(true);
+    expect(writeText).toHaveBeenCalledWith("abc");
+  });
+
+  test("copyTextToClipboard: 无 document 时 fallback 直接返回 false", async () => {
+    stubSecureContext(false);
+    Object.defineProperty(navigator, "clipboard", { value: undefined, configurable: true });
+    vi.stubGlobal("document", undefined as unknown as Document);
+
+    await expect(copyTextToClipboard("abc")).resolves.toBe(false);
+  });
+});

+ 22 - 0
tests/unit/proxy/chat-completions-handler-guard-pipeline.test.ts

@@ -232,6 +232,28 @@ beforeEach(() => {
 });
 
 describe("handleChatCompletions:必须走 GuardPipeline", () => {
+  test("pipeline 早退错误时,应附带 x-cch-session-id 且 message 追加 cch_session_id", async () => {
+    h.session = createSession({
+      model: "gpt-4.1-mini",
+      messages: [{ role: "user", content: "hi" }],
+    });
+    h.session.sessionId = "s_123";
+    h.clientGuardResult = new Response(
+      JSON.stringify({
+        error: { message: "client blocked", type: "invalid_request_error", code: "client_blocked" },
+      }),
+      { status: 400, headers: { "Content-Type": "application/json" } }
+    );
+
+    const { handleChatCompletions } = await import("@/app/v1/_lib/codex/chat-completions-handler");
+    const res = await handleChatCompletions({} as any);
+
+    expect(res.status).toBe(400);
+    expect(res.headers.get("x-cch-session-id")).toBe("s_123");
+    const body = await res.json();
+    expect(body.error.message).toBe("client blocked (cch_session_id: s_123)");
+  });
+
   test("请求体既不是 messages 也不是 input 时,应返回 400(不进入 pipeline)", async () => {
     h.session = createSession({});
 

+ 24 - 0
tests/unit/proxy/error-handler-session-id-error.test.ts

@@ -0,0 +1,24 @@
+import { describe, expect, test } from "vitest";
+import { ProxyErrorHandler } from "@/app/v1/_lib/proxy/error-handler";
+
+describe("ProxyErrorHandler.handle - session id on errors", () => {
+  test("decorates error response with x-cch-session-id and message suffix", async () => {
+    const session = {
+      sessionId: "s_123",
+      messageContext: null,
+      startTime: Date.now(),
+      getProviderChain: () => [],
+      getCurrentModel: () => null,
+      getContext1mApplied: () => false,
+      provider: null,
+    } as any;
+
+    const res = await ProxyErrorHandler.handle(session, new Error("boom"));
+
+    expect(res.status).toBe(500);
+    expect(res.headers.get("x-cch-session-id")).toBe("s_123");
+
+    const body = await res.json();
+    expect(body.error.message).toBe("boom (cch_session_id: s_123)");
+  });
+});

+ 207 - 0
tests/unit/proxy/proxy-handler-session-id-error.test.ts

@@ -0,0 +1,207 @@
+import { describe, expect, test, vi } from "vitest";
+import { ProxyResponses } from "@/app/v1/_lib/proxy/responses";
+import { ProxyError } from "@/app/v1/_lib/proxy/errors";
+
+const h = vi.hoisted(() => ({
+  session: {
+    originalFormat: "openai",
+    sessionId: "s_123",
+    requestUrl: new URL("http://localhost/v1/messages"),
+    request: {
+      model: "gpt",
+      message: {},
+    },
+    isCountTokensRequest: () => false,
+    setOriginalFormat: () => {},
+    messageContext: null,
+    provider: null,
+  } as any,
+
+  fromContextError: null as unknown,
+  pipelineError: null as unknown,
+  earlyResponse: null as Response | null,
+  forwardResponse: new Response("ok", { status: 200 }),
+  dispatchedResponse: null as Response | null,
+
+  endpointFormat: null as string | null,
+  trackerCalls: [] as string[],
+}));
+
+vi.mock("@/app/v1/_lib/proxy/session", () => ({
+  ProxySession: {
+    fromContext: async () => {
+      if (h.fromContextError) throw h.fromContextError;
+      return h.session;
+    },
+  },
+}));
+
+vi.mock("@/app/v1/_lib/proxy/guard-pipeline", () => ({
+  RequestType: { CHAT: "CHAT", COUNT_TOKENS: "COUNT_TOKENS" },
+  GuardPipelineBuilder: {
+    fromRequestType: () => ({
+      run: async () => {
+        if (h.pipelineError) throw h.pipelineError;
+        return h.earlyResponse;
+      },
+    }),
+  },
+}));
+
+vi.mock("@/app/v1/_lib/proxy/format-mapper", () => ({
+  detectClientFormat: () => "openai",
+  detectFormatByEndpoint: () => h.endpointFormat,
+}));
+
+vi.mock("@/app/v1/_lib/proxy/forwarder", () => ({
+  ProxyForwarder: {
+    send: async () => h.forwardResponse,
+  },
+}));
+
+vi.mock("@/app/v1/_lib/proxy/response-handler", () => ({
+  ProxyResponseHandler: {
+    dispatch: async () => h.dispatchedResponse ?? h.forwardResponse,
+  },
+}));
+
+vi.mock("@/app/v1/_lib/proxy/error-handler", () => ({
+  ProxyErrorHandler: {
+    handle: async () => new Response("handled", { status: 502 }),
+  },
+}));
+
+vi.mock("@/lib/session-tracker", () => ({
+  SessionTracker: {
+    incrementConcurrentCount: async () => {
+      h.trackerCalls.push("inc");
+    },
+    decrementConcurrentCount: async () => {
+      h.trackerCalls.push("dec");
+    },
+  },
+}));
+
+vi.mock("@/lib/proxy-status-tracker", () => ({
+  ProxyStatusTracker: {
+    getInstance: () => ({
+      startRequest: () => {
+        h.trackerCalls.push("startRequest");
+      },
+      endRequest: () => {},
+    }),
+  },
+}));
+
+describe("handleProxyRequest - session id on errors", async () => {
+  const { handleProxyRequest } = await import("@/app/v1/_lib/proxy-handler");
+
+  test("decorates early error response with x-cch-session-id and message suffix", async () => {
+    h.fromContextError = null;
+    h.session.originalFormat = "openai";
+    h.endpointFormat = null;
+    h.trackerCalls.length = 0;
+    h.pipelineError = null;
+    h.earlyResponse = ProxyResponses.buildError(400, "bad request");
+    const res = await handleProxyRequest({} as any);
+
+    expect(res.status).toBe(400);
+    expect(res.headers.get("x-cch-session-id")).toBe("s_123");
+
+    const body = await res.json();
+    expect(body.error.message).toBe("bad request (cch_session_id: s_123)");
+  });
+
+  test("decorates dispatch error response with x-cch-session-id and message suffix", async () => {
+    h.fromContextError = null;
+    h.session.originalFormat = "openai";
+    h.endpointFormat = null;
+    h.trackerCalls.length = 0;
+    h.pipelineError = null;
+    h.earlyResponse = null;
+    h.forwardResponse = new Response("upstream", { status: 502 });
+    h.dispatchedResponse = ProxyResponses.buildError(502, "bad gateway");
+
+    const res = await handleProxyRequest({} as any);
+
+    expect(res.status).toBe(502);
+    expect(res.headers.get("x-cch-session-id")).toBe("s_123");
+
+    const body = await res.json();
+    expect(body.error.message).toBe("bad gateway (cch_session_id: s_123)");
+  });
+
+  test("covers claude format detection branch without breaking behavior", async () => {
+    h.fromContextError = null;
+    h.session.originalFormat = "claude";
+    h.endpointFormat = null;
+    h.trackerCalls.length = 0;
+    h.pipelineError = null;
+    h.earlyResponse = ProxyResponses.buildError(400, "bad request");
+    h.session.requestUrl = new URL("http://localhost/v1/unknown");
+    h.session.request = { model: "gpt", message: { contents: [] } };
+
+    const res = await handleProxyRequest({} as any);
+    expect(res.status).toBe(400);
+    expect(res.headers.get("x-cch-session-id")).toBe("s_123");
+  });
+
+  test("covers endpoint format detection + tracking + finally decrement", async () => {
+    h.fromContextError = null;
+    h.session.originalFormat = "claude";
+    h.endpointFormat = "openai";
+    h.trackerCalls.length = 0;
+    h.pipelineError = null;
+    h.earlyResponse = null;
+    h.forwardResponse = new Response("ok", { status: 200 });
+    h.dispatchedResponse = null;
+
+    h.session.sessionId = "s_123";
+    h.session.messageContext = { id: 1, user: { id: 1, name: "u" }, key: { name: "k" } };
+    h.session.provider = { id: 1, name: "p" };
+    h.session.isCountTokensRequest = () => false;
+
+    const res = await handleProxyRequest({} as any);
+    expect(res.status).toBe(200);
+    expect(h.trackerCalls).toEqual(["inc", "startRequest", "dec"]);
+  });
+
+  test("session not created and ProxyError thrown: returns buildError without session header", async () => {
+    h.fromContextError = new ProxyError("upstream", 401);
+    h.endpointFormat = null;
+    h.trackerCalls.length = 0;
+    h.pipelineError = null;
+    h.earlyResponse = null;
+
+    const res = await handleProxyRequest({} as any);
+    expect(res.status).toBe(401);
+    expect(res.headers.get("x-cch-session-id")).toBeNull();
+    const body = await res.json();
+    expect(body.error.message).toBe("upstream");
+  });
+
+  test("session created but pipeline throws: routes to ProxyErrorHandler.handle", async () => {
+    h.fromContextError = null;
+    h.endpointFormat = null;
+    h.trackerCalls.length = 0;
+    h.pipelineError = new Error("pipeline boom");
+    h.earlyResponse = null;
+
+    const res = await handleProxyRequest({} as any);
+    expect(res.status).toBe(502);
+    expect(await res.text()).toBe("handled");
+  });
+
+  test("session not created and non-ProxyError thrown: returns 500 buildError", async () => {
+    h.fromContextError = new Error("boom");
+    h.endpointFormat = null;
+    h.trackerCalls.length = 0;
+    h.pipelineError = null;
+    h.earlyResponse = null;
+
+    const res = await handleProxyRequest({} as any);
+    expect(res.status).toBe(500);
+    const body = await res.json();
+    expect(body.error.message).toBe("代理请求发生未知错误");
+  });
+});

+ 75 - 0
tests/unit/proxy/responses-session-id.test.ts

@@ -0,0 +1,75 @@
+import { describe, expect, test } from "vitest";
+import { ProxyResponses } from "@/app/v1/_lib/proxy/responses";
+import { attachSessionIdToErrorResponse } from "@/app/v1/_lib/proxy/error-session-id";
+
+describe("ProxyResponses.attachSessionIdToErrorResponse", () => {
+  test("adds x-cch-session-id and appends to error.message for JSON error responses", async () => {
+    const response = ProxyResponses.buildError(400, "bad request");
+    const decorated = await attachSessionIdToErrorResponse("s_123", response);
+
+    expect(decorated.status).toBe(400);
+    expect(decorated.headers.get("x-cch-session-id")).toBe("s_123");
+
+    const body = await decorated.json();
+    expect(body.error.message).toBe("bad request (cch_session_id: s_123)");
+  });
+
+  test("does nothing when sessionId is missing", async () => {
+    const response = ProxyResponses.buildError(400, "bad request");
+    const decorated = await attachSessionIdToErrorResponse(undefined, response);
+
+    expect(decorated).toBe(response);
+  });
+
+  test("does nothing for non-error responses", async () => {
+    const response = new Response(JSON.stringify({ ok: true }), {
+      status: 200,
+      headers: { "Content-Type": "application/json" },
+    });
+    const decorated = await attachSessionIdToErrorResponse("s_123", response);
+
+    expect(decorated).toBe(response);
+  });
+
+  test("does not double-append when message already contains cch_session_id", async () => {
+    const response = ProxyResponses.buildError(400, "bad request (cch_session_id: s_123)");
+    const decorated = await attachSessionIdToErrorResponse("s_123", response);
+
+    const body = await decorated.json();
+    expect(body.error.message).toBe("bad request (cch_session_id: s_123)");
+  });
+
+  test("adds header for non-json error responses (body unchanged)", async () => {
+    const response = new Response("oops", {
+      status: 500,
+      headers: { "Content-Type": "text/plain" },
+    });
+    const decorated = await attachSessionIdToErrorResponse("s_123", response);
+
+    expect(decorated.status).toBe(500);
+    expect(decorated.headers.get("x-cch-session-id")).toBe("s_123");
+    expect(await decorated.text()).toBe("oops");
+  });
+
+  test("adds header for json without error.message (body unchanged)", async () => {
+    const response = new Response(JSON.stringify({ foo: "bar" }), {
+      status: 500,
+      headers: { "Content-Type": "application/json" },
+    });
+    const decorated = await attachSessionIdToErrorResponse("s_123", response);
+
+    expect(decorated.headers.get("x-cch-session-id")).toBe("s_123");
+    expect(await decorated.json()).toEqual({ foo: "bar" });
+  });
+
+  test("adds header for SSE error responses (no body rewrite)", async () => {
+    const response = new Response("data: hi\n\n", {
+      status: 500,
+      headers: { "Content-Type": "text/event-stream" },
+    });
+    const decorated = await attachSessionIdToErrorResponse("s_123", response);
+
+    expect(decorated.headers.get("x-cch-session-id")).toBe("s_123");
+    expect(await decorated.text()).toBe("data: hi\n\n");
+  });
+});

+ 23 - 0
tests/unit/repository/escape-like.test.ts

@@ -0,0 +1,23 @@
+import { describe, expect, test } from "vitest";
+
+import { escapeLike } from "@/repository/_shared/like";
+
+describe("escapeLike", () => {
+  test("普通字符串保持不变", () => {
+    expect(escapeLike("abc-123")).toBe("abc-123");
+  });
+
+  test("%/_/\\\\ 应被转义(用于 LIKE ... ESCAPE '\\\\')", () => {
+    expect(escapeLike("%")).toBe("\\%");
+    expect(escapeLike("_")).toBe("\\_");
+    expect(escapeLike("\\")).toBe("\\\\");
+  });
+
+  test("组合输入应按字面量匹配语义转义", () => {
+    expect(escapeLike("a%b_c\\d")).toBe("a\\%b\\_c\\\\d");
+  });
+
+  test("空字符串应返回空字符串", () => {
+    expect(escapeLike("")).toBe("");
+  });
+});

+ 309 - 0
tests/unit/repository/usage-logs-sessionid-filter.test.ts

@@ -0,0 +1,309 @@
+import { describe, expect, test, vi } from "vitest";
+
+function sqlToString(sqlObj: unknown): string {
+  const visited = new Set<unknown>();
+
+  const walk = (node: unknown): string => {
+    if (!node || visited.has(node)) return "";
+    visited.add(node);
+
+    if (typeof node === "string") return node;
+
+    if (typeof node === "object") {
+      const anyNode = node as any;
+      if (Array.isArray(anyNode)) {
+        return anyNode.map(walk).join("");
+      }
+
+      if (anyNode.value) {
+        if (Array.isArray(anyNode.value)) {
+          return anyNode.value.map(String).join("");
+        }
+        return String(anyNode.value);
+      }
+
+      if (anyNode.queryChunks) {
+        return walk(anyNode.queryChunks);
+      }
+    }
+
+    return "";
+  };
+
+  return walk(sqlObj);
+}
+
+function createThenableQuery<T>(result: T, whereArgs?: unknown[]) {
+  const query: any = Promise.resolve(result);
+
+  query.from = vi.fn(() => query);
+  query.innerJoin = vi.fn(() => query);
+  query.leftJoin = vi.fn(() => query);
+  query.orderBy = vi.fn(() => query);
+  query.limit = vi.fn(() => query);
+  query.offset = vi.fn(() => query);
+  query.groupBy = vi.fn(() => query);
+  query.where = vi.fn((arg: unknown) => {
+    whereArgs?.push(arg);
+    return query;
+  });
+
+  return query;
+}
+
+describe("Usage logs sessionId filter", () => {
+  test("findUsageLogsBatch: sessionId 为空/空白不应追加条件", async () => {
+    vi.resetModules();
+
+    const whereArgs: unknown[] = [];
+    const selectMock = vi.fn(() => createThenableQuery([], whereArgs));
+
+    vi.doMock("@/drizzle/db", () => ({
+      db: {
+        select: selectMock,
+        execute: vi.fn(async () => ({ count: 0 })),
+      },
+    }));
+
+    const { findUsageLogsBatch } = await import("@/repository/usage-logs");
+    await findUsageLogsBatch({});
+    await findUsageLogsBatch({ sessionId: "   " });
+
+    expect(whereArgs).toHaveLength(2);
+    const baseWhereSql = sqlToString(whereArgs[0]).toLowerCase();
+    const blankWhereSql = sqlToString(whereArgs[1]).toLowerCase();
+    expect(blankWhereSql).toBe(baseWhereSql);
+  });
+
+  test("findUsageLogsBatch: sessionId 应 trim 后精确匹配", async () => {
+    vi.resetModules();
+
+    const whereArgs: unknown[] = [];
+    const selectMock = vi.fn(() => createThenableQuery([], whereArgs));
+
+    vi.doMock("@/drizzle/db", () => ({
+      db: {
+        select: selectMock,
+        execute: vi.fn(async () => ({ count: 0 })),
+      },
+    }));
+
+    const { findUsageLogsBatch } = await import("@/repository/usage-logs");
+    await findUsageLogsBatch({ sessionId: "  abc  " });
+
+    expect(whereArgs.length).toBeGreaterThan(0);
+    const whereSql = sqlToString(whereArgs[0]).toLowerCase();
+    expect(whereSql).toContain("abc");
+    expect(whereSql).not.toContain("  abc  ");
+  });
+
+  test("findUsageLogsWithDetails: sessionId 为空/空白不应追加条件", async () => {
+    vi.resetModules();
+
+    const whereArgs: unknown[] = [];
+    const selectQueue: any[] = [];
+    selectQueue.push(
+      createThenableQuery(
+        [
+          {
+            totalRows: 0,
+            totalRequests: 0,
+            totalCost: "0",
+            totalInputTokens: 0,
+            totalOutputTokens: 0,
+            totalCacheCreationTokens: 0,
+            totalCacheReadTokens: 0,
+            totalCacheCreation5mTokens: 0,
+            totalCacheCreation1hTokens: 0,
+          },
+        ],
+        whereArgs
+      )
+    );
+    selectQueue.push(createThenableQuery([]));
+    selectQueue.push(
+      createThenableQuery(
+        [
+          {
+            totalRows: 0,
+            totalRequests: 0,
+            totalCost: "0",
+            totalInputTokens: 0,
+            totalOutputTokens: 0,
+            totalCacheCreationTokens: 0,
+            totalCacheReadTokens: 0,
+            totalCacheCreation5mTokens: 0,
+            totalCacheCreation1hTokens: 0,
+          },
+        ],
+        whereArgs
+      )
+    );
+    selectQueue.push(createThenableQuery([]));
+
+    const fallbackSelect = createThenableQuery<unknown[]>([]);
+    const selectMock = vi.fn(() => selectQueue.shift() ?? fallbackSelect);
+
+    vi.doMock("@/drizzle/db", () => ({
+      db: {
+        select: selectMock,
+        execute: vi.fn(async () => ({ count: 0 })),
+      },
+    }));
+
+    const { findUsageLogsWithDetails } = await import("@/repository/usage-logs");
+    await findUsageLogsWithDetails({ page: 1, pageSize: 1 });
+    await findUsageLogsWithDetails({ page: 1, pageSize: 1, sessionId: "  " });
+
+    expect(whereArgs).toHaveLength(2);
+    const baseWhereSql = sqlToString(whereArgs[0]).toLowerCase();
+    const blankWhereSql = sqlToString(whereArgs[1]).toLowerCase();
+    expect(blankWhereSql).toBe(baseWhereSql);
+  });
+
+  test("findUsageLogsWithDetails: sessionId 应 trim 后精确匹配", async () => {
+    vi.resetModules();
+
+    const whereArgs: unknown[] = [];
+    const selectQueue: any[] = [];
+    selectQueue.push(
+      createThenableQuery(
+        [
+          {
+            totalRows: 0,
+            totalRequests: 0,
+            totalCost: "0",
+            totalInputTokens: 0,
+            totalOutputTokens: 0,
+            totalCacheCreationTokens: 0,
+            totalCacheReadTokens: 0,
+            totalCacheCreation5mTokens: 0,
+            totalCacheCreation1hTokens: 0,
+          },
+        ],
+        whereArgs
+      )
+    );
+    selectQueue.push(createThenableQuery([]));
+
+    const fallbackSelect = createThenableQuery<unknown[]>([]);
+    const selectMock = vi.fn(() => selectQueue.shift() ?? fallbackSelect);
+
+    vi.doMock("@/drizzle/db", () => ({
+      db: {
+        select: selectMock,
+        execute: vi.fn(async () => ({ count: 0 })),
+      },
+    }));
+
+    const { findUsageLogsWithDetails } = await import("@/repository/usage-logs");
+    await findUsageLogsWithDetails({ page: 1, pageSize: 1, sessionId: "  abc  " });
+
+    expect(whereArgs.length).toBeGreaterThan(0);
+    const whereSql = sqlToString(whereArgs[0]).toLowerCase();
+    expect(whereSql).toContain("abc");
+    expect(whereSql).not.toContain("  abc  ");
+  });
+
+  test("findUsageLogsStats: sessionId 为空/空白不应追加条件", async () => {
+    vi.resetModules();
+
+    const whereArgs: unknown[] = [];
+    const selectQueue: any[] = [];
+    selectQueue.push(
+      createThenableQuery(
+        [
+          {
+            totalRequests: 0,
+            totalCost: "0",
+            totalInputTokens: 0,
+            totalOutputTokens: 0,
+            totalCacheCreationTokens: 0,
+            totalCacheReadTokens: 0,
+            totalCacheCreation5mTokens: 0,
+            totalCacheCreation1hTokens: 0,
+          },
+        ],
+        whereArgs
+      )
+    );
+    selectQueue.push(
+      createThenableQuery(
+        [
+          {
+            totalRequests: 0,
+            totalCost: "0",
+            totalInputTokens: 0,
+            totalOutputTokens: 0,
+            totalCacheCreationTokens: 0,
+            totalCacheReadTokens: 0,
+            totalCacheCreation5mTokens: 0,
+            totalCacheCreation1hTokens: 0,
+          },
+        ],
+        whereArgs
+      )
+    );
+
+    const fallbackSelect = createThenableQuery<unknown[]>([]);
+    const selectMock = vi.fn(() => selectQueue.shift() ?? fallbackSelect);
+
+    vi.doMock("@/drizzle/db", () => ({
+      db: {
+        select: selectMock,
+        execute: vi.fn(async () => ({ count: 0 })),
+      },
+    }));
+
+    const { findUsageLogsStats } = await import("@/repository/usage-logs");
+    await findUsageLogsStats({});
+    await findUsageLogsStats({ sessionId: "  " });
+
+    expect(whereArgs).toHaveLength(2);
+    const baseWhereSql = sqlToString(whereArgs[0]).toLowerCase();
+    const blankWhereSql = sqlToString(whereArgs[1]).toLowerCase();
+    expect(blankWhereSql).toBe(baseWhereSql);
+  });
+
+  test("findUsageLogsStats: sessionId 应 trim 后精确匹配", async () => {
+    vi.resetModules();
+
+    const whereArgs: unknown[] = [];
+    const selectQueue: any[] = [];
+    selectQueue.push(
+      createThenableQuery(
+        [
+          {
+            totalRequests: 0,
+            totalCost: "0",
+            totalInputTokens: 0,
+            totalOutputTokens: 0,
+            totalCacheCreationTokens: 0,
+            totalCacheReadTokens: 0,
+            totalCacheCreation5mTokens: 0,
+            totalCacheCreation1hTokens: 0,
+          },
+        ],
+        whereArgs
+      )
+    );
+
+    const fallbackSelect = createThenableQuery<unknown[]>([]);
+    const selectMock = vi.fn(() => selectQueue.shift() ?? fallbackSelect);
+
+    vi.doMock("@/drizzle/db", () => ({
+      db: {
+        select: selectMock,
+        execute: vi.fn(async () => ({ count: 0 })),
+      },
+    }));
+
+    const { findUsageLogsStats } = await import("@/repository/usage-logs");
+    await findUsageLogsStats({ sessionId: "  abc  " });
+
+    expect(whereArgs.length).toBeGreaterThan(0);
+    const whereSql = sqlToString(whereArgs[0]).toLowerCase();
+    expect(whereSql).toContain("abc");
+    expect(whereSql).not.toContain("  abc  ");
+  });
+});

+ 204 - 0
tests/unit/repository/usage-logs-sessionid-suggestions.test.ts

@@ -0,0 +1,204 @@
+import { describe, expect, test, vi } from "vitest";
+
+function sqlToString(sqlObj: unknown): string {
+  const visited = new Set<unknown>();
+
+  const walk = (node: unknown): string => {
+    if (!node || visited.has(node)) return "";
+    visited.add(node);
+
+    if (typeof node === "string") return node;
+
+    if (typeof node === "object") {
+      const anyNode = node as any;
+      if (Array.isArray(anyNode)) {
+        return anyNode.map(walk).join("");
+      }
+
+      if (anyNode.value) {
+        if (Array.isArray(anyNode.value)) {
+          return anyNode.value.map(String).join("");
+        }
+        return String(anyNode.value);
+      }
+
+      if (anyNode.queryChunks) {
+        return walk(anyNode.queryChunks);
+      }
+    }
+
+    return "";
+  };
+
+  return walk(sqlObj);
+}
+
+function createThenableQuery<T>(
+  result: T,
+  opts?: {
+    whereArgs?: unknown[];
+    groupByArgs?: unknown[];
+    orderByArgs?: unknown[];
+    limitArgs?: unknown[];
+  }
+) {
+  const query: any = Promise.resolve(result);
+
+  query.from = vi.fn(() => query);
+  query.innerJoin = vi.fn(() => query);
+  query.leftJoin = vi.fn(() => query);
+  query.where = vi.fn((arg: unknown) => {
+    opts?.whereArgs?.push(arg);
+    return query;
+  });
+  query.groupBy = vi.fn((...args: unknown[]) => {
+    opts?.groupByArgs?.push(args);
+    return query;
+  });
+  query.orderBy = vi.fn((...args: unknown[]) => {
+    opts?.orderByArgs?.push(args);
+    return query;
+  });
+  query.limit = vi.fn((arg: unknown) => {
+    opts?.limitArgs?.push(arg);
+    return query;
+  });
+
+  return query;
+}
+
+describe("Usage logs sessionId suggestions", () => {
+  test("term 为空/空白:应直接返回空数组且不查询 DB", async () => {
+    vi.resetModules();
+
+    const selectMock = vi.fn(() => createThenableQuery([]));
+    vi.doMock("@/drizzle/db", () => ({
+      db: { select: selectMock },
+    }));
+
+    const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs");
+    const result = await findUsageLogSessionIdSuggestions({ term: "   " });
+
+    expect(result).toEqual([]);
+    expect(selectMock).not.toHaveBeenCalled();
+  });
+
+  test("term 应 trim 并按 MIN(created_at) 倒序,limit 生效", async () => {
+    vi.resetModules();
+
+    const whereArgs: unknown[] = [];
+    const groupByArgs: unknown[] = [];
+    const orderByArgs: unknown[] = [];
+    const limitArgs: unknown[] = [];
+    const selectMock = vi.fn(() =>
+      createThenableQuery(
+        [
+          { sessionId: "session_1", firstSeen: new Date("2026-01-01T00:00:00Z") },
+          { sessionId: null, firstSeen: new Date("2026-01-01T00:00:00Z") },
+        ],
+        { whereArgs, groupByArgs, orderByArgs, limitArgs }
+      )
+    );
+
+    vi.doMock("@/drizzle/db", () => ({
+      db: { select: selectMock },
+    }));
+
+    const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs");
+    const result = await findUsageLogSessionIdSuggestions({
+      term: "  abc  ",
+      userId: 1,
+      keyId: 2,
+      providerId: 3,
+      limit: 20,
+    });
+
+    expect(result).toEqual(["session_1"]);
+
+    expect(whereArgs.length).toBeGreaterThan(0);
+    const whereSql = sqlToString(whereArgs[0]).toLowerCase();
+    expect(whereSql).toContain("like");
+    expect(whereSql).toContain("escape");
+    expect(whereSql).toContain("abc%");
+    expect(whereSql).not.toContain("%abc%");
+    expect(whereSql).not.toContain("ilike");
+    expect(whereSql).not.toContain("  abc  ");
+
+    expect(groupByArgs.length).toBeGreaterThan(0);
+
+    expect(orderByArgs.length).toBeGreaterThan(0);
+    const orderSql = sqlToString(orderByArgs[0]).toLowerCase();
+    expect(orderSql).toContain("min");
+
+    expect(limitArgs).toEqual([20]);
+  });
+
+  test("term 含 %/_/\\\\:应按字面量前缀匹配(需转义)", async () => {
+    vi.resetModules();
+
+    const whereArgs: unknown[] = [];
+    const selectMock = vi.fn(() => createThenableQuery([], { whereArgs }));
+
+    vi.doMock("@/drizzle/db", () => ({
+      db: { select: selectMock },
+    }));
+
+    const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs");
+    await findUsageLogSessionIdSuggestions({
+      term: "a%_\\b",
+      limit: 20,
+    });
+
+    expect(whereArgs.length).toBeGreaterThan(0);
+    const whereSql = sqlToString(whereArgs[0]).toLowerCase();
+    expect(whereSql).toContain("like");
+    expect(whereSql).toContain("escape");
+    expect(whereSql).toContain("a\\%\\_\\\\b%");
+    expect(whereSql).not.toContain("ilike");
+  });
+
+  test("limit 应被 clamp 到 [1, 50]", async () => {
+    vi.resetModules();
+
+    const limitArgs: unknown[] = [];
+    const selectMock = vi.fn(() => createThenableQuery([], { limitArgs }));
+    vi.doMock("@/drizzle/db", () => ({
+      db: { select: selectMock },
+    }));
+
+    const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs");
+    await findUsageLogSessionIdSuggestions({ term: "abc", limit: 500 });
+
+    expect(limitArgs).toEqual([50]);
+  });
+
+  test("keyId 未提供时不应 innerJoin(keysTable)", async () => {
+    vi.resetModules();
+
+    const query = createThenableQuery([]);
+    const selectMock = vi.fn(() => query);
+    vi.doMock("@/drizzle/db", () => ({
+      db: { select: selectMock },
+    }));
+
+    const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs");
+    await findUsageLogSessionIdSuggestions({ term: "abc", limit: 20 });
+
+    expect(query.innerJoin).not.toHaveBeenCalled();
+  });
+
+  test("keyId 提供时才 innerJoin(keysTable)", async () => {
+    vi.resetModules();
+
+    const query = createThenableQuery([]);
+    const selectMock = vi.fn(() => query);
+    vi.doMock("@/drizzle/db", () => ({
+      db: { select: selectMock },
+    }));
+
+    const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs");
+    await findUsageLogSessionIdSuggestions({ term: "abc", keyId: 2, limit: 20 });
+
+    expect(query.innerJoin).toHaveBeenCalledTimes(1);
+  });
+});

+ 59 - 0
vitest.include-session-id-in-errors.config.ts

@@ -0,0 +1,59 @@
+import path from "node:path";
+import { defineConfig } from "vitest/config";
+
+/**
+ * Include CCH session id in client errors - scoped coverage config
+ *
+ * 目的:
+ * - 验证错误响应中附带 sessionId 的行为(message + header)
+ * - 覆盖率只统计本次改动相关模块,避免引入 Next/DB/Redis 重模块
+ * - 覆盖率阈值:>= 90%
+ */
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: "happy-dom",
+    setupFiles: ["./tests/setup.ts"],
+
+    include: [
+      "tests/unit/proxy/responses-session-id.test.ts",
+      "tests/unit/proxy/proxy-handler-session-id-error.test.ts",
+      "tests/unit/proxy/error-handler-session-id-error.test.ts",
+      "tests/unit/proxy/chat-completions-handler-guard-pipeline.test.ts",
+    ],
+    exclude: ["node_modules", ".next", "dist", "build", "coverage", "tests/integration/**"],
+
+    coverage: {
+      provider: "v8",
+      reporter: ["text", "html", "json"],
+      reportsDirectory: "./coverage-include-session-id-in-errors",
+
+      include: [
+        "src/app/v1/_lib/proxy/error-session-id.ts",
+        "src/app/v1/_lib/proxy-handler.ts",
+        "src/app/v1/_lib/codex/chat-completions-handler.ts",
+      ],
+      exclude: ["node_modules/", "tests/", "**/*.d.ts", ".next/"],
+
+      thresholds: {
+        lines: 90,
+        functions: 90,
+        branches: 90,
+        statements: 90,
+      },
+    },
+
+    reporters: ["verbose"],
+    isolate: true,
+    mockReset: true,
+    restoreMocks: true,
+    clearMocks: true,
+  },
+
+  resolve: {
+    alias: {
+      "@": path.resolve(__dirname, "./src"),
+      "server-only": path.resolve(__dirname, "./tests/server-only.mock.ts"),
+    },
+  },
+});

+ 61 - 0
vitest.logs-sessionid-time-filter.config.ts

@@ -0,0 +1,61 @@
+import path from "node:path";
+import { defineConfig } from "vitest/config";
+
+/**
+ * Dashboard Logs(Session ID + 秒级时间筛选)专项覆盖率配置
+ *
+ * 目的:
+ * - 仅统计本需求可纯函数化/可隔离模块的覆盖率(>= 90%)
+ * - 仍然执行关键回归相关的单测集合,避免只跑“指标好看”的子集
+ */
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: "happy-dom",
+    setupFiles: ["./tests/setup.ts"],
+
+    include: [
+      "tests/unit/repository/usage-logs-sessionid-filter.test.ts",
+      "tests/unit/repository/usage-logs-sessionid-suggestions.test.ts",
+      "tests/unit/dashboard-logs-query-utils.test.ts",
+      "tests/unit/dashboard-logs-time-range-utils.test.ts",
+      "tests/unit/dashboard-logs-filters-time-range.test.tsx",
+      "tests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsx",
+      "tests/unit/dashboard-logs-virtualized-special-settings-ui.test.tsx",
+      "src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx",
+    ],
+    exclude: ["node_modules", ".next", "dist", "build", "coverage", "tests/integration/**"],
+
+    coverage: {
+      provider: "v8",
+      reporter: ["text", "html", "json", "lcov"],
+      reportsDirectory: "./coverage-logs-sessionid-time-filter",
+
+      include: [
+        "src/app/[locale]/dashboard/logs/_utils/logs-query.ts",
+        "src/app/[locale]/dashboard/logs/_utils/time-range.ts",
+      ],
+      exclude: ["node_modules/", "tests/", "**/*.d.ts", ".next/"],
+
+      thresholds: {
+        lines: 90,
+        functions: 90,
+        branches: 90,
+        statements: 90,
+      },
+    },
+
+    reporters: ["verbose"],
+    isolate: true,
+    mockReset: true,
+    restoreMocks: true,
+    clearMocks: true,
+  },
+
+  resolve: {
+    alias: {
+      "@": path.resolve(__dirname, "./src"),
+      "server-only": path.resolve(__dirname, "./tests/server-only.mock.ts"),
+    },
+  },
+});

+ 60 - 0
vitest.usage-logs-sessionid-search.config.ts

@@ -0,0 +1,60 @@
+import path from "node:path";
+import { defineConfig } from "vitest/config";
+
+/**
+ * Dashboard Logs(Session ID 搜索:前缀匹配 + LIKE 转义)专项覆盖率配置
+ *
+ * 目的:
+ * - 仅统计本需求可隔离模块的覆盖率(>= 90%)
+ * - 同时执行关联单测集合,避免只跑“指标好看”的子集
+ */
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: "happy-dom",
+    setupFiles: ["./tests/setup.ts"],
+
+    include: [
+      "tests/unit/repository/usage-logs-sessionid-suggestions.test.ts",
+      "tests/unit/repository/usage-logs-sessionid-filter.test.ts",
+      "tests/unit/repository/warmup-stats-exclusion.test.ts",
+      "tests/unit/repository/escape-like.test.ts",
+      "tests/unit/lib/constants/usage-logs.constants.test.ts",
+      "tests/unit/lib/utils/clipboard.test.ts",
+    ],
+    exclude: ["node_modules", ".next", "dist", "build", "coverage", "tests/integration/**"],
+
+    coverage: {
+      provider: "v8",
+      reporter: ["text", "html", "json", "lcov"],
+      reportsDirectory: "./coverage-usage-logs-sessionid-search",
+
+      include: [
+        "src/repository/_shared/like.ts",
+        "src/lib/constants/usage-logs.constants.ts",
+        "src/lib/utils/clipboard.ts",
+      ],
+      exclude: ["node_modules/", "tests/", "**/*.d.ts", ".next/"],
+
+      thresholds: {
+        lines: 90,
+        functions: 90,
+        branches: 90,
+        statements: 90,
+      },
+    },
+
+    reporters: ["verbose"],
+    isolate: true,
+    mockReset: true,
+    restoreMocks: true,
+    clearMocks: true,
+  },
+
+  resolve: {
+    alias: {
+      "@": path.resolve(__dirname, "./src"),
+      "server-only": path.resolve(__dirname, "./tests/server-only.mock.ts"),
+    },
+  },
+});