Quellcode durchsuchen

feat(pricing): resolve provider-aware billing for multi-provider models (#873)

* feat(pricing): resolve provider-aware billing for multi-provider models

* chore: format code (feat-codex-1m-11ecf3a)

* fix: auto-fix CI formatting issues (#874)

Fixed:
- Import ordering (alphabetical) in MetadataTab.tsx, SummaryTab.tsx, price-list.tsx, response-handler.ts, session.ts
- Changed 'let' to 'const' for never-reassigned variable in response-handler.ts

CI Run: https://github.com/ding113/claude-code-hub/actions/runs/22762234077

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <[email protected]>

* fix(pricing): address review feedback on billing resolution

* fix(pricing): address remaining review feedback

* chore: format code (feat-codex-1m-e50c32b)

* feat(pricing): support codex fast service tier billing

* chore: format code (feat-codex-1m-289fe7b)

* fix(logs): restore context1m audit fallback compatibility

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <[email protected]>
Ding vor 1 Monat
Ursprung
Commit
2e663cd5c6
37 geänderte Dateien mit 2776 neuen und 574 gelöschten Zeilen
  1. 26 6
      messages/en/dashboard.json
  2. 16 2
      messages/en/settings/prices.json
  3. 26 6
      messages/ja/dashboard.json
  4. 14 1
      messages/ja/settings/prices.json
  5. 26 6
      messages/ru/dashboard.json
  6. 14 1
      messages/ru/settings/prices.json
  7. 26 6
      messages/zh-CN/dashboard.json
  8. 16 2
      messages/zh-CN/settings/prices.json
  9. 26 6
      messages/zh-TW/dashboard.json
  10. 14 1
      messages/zh-TW/settings/prices.json
  11. 78 1
      src/actions/model-prices.ts
  12. 40 0
      src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx
  13. 40 0
      src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx
  14. 34 0
      src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx
  15. 18 1
      src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
  16. 20 1
      src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
  17. 225 182
      src/app/[locale]/settings/prices/_components/price-list.tsx
  18. 197 0
      src/app/[locale]/settings/prices/_components/provider-pricing-dialog.tsx
  19. 11 1
      src/app/v1/_lib/proxy/forwarder.ts
  20. 246 115
      src/app/v1/_lib/proxy/response-handler.ts
  21. 59 37
      src/app/v1/_lib/proxy/session.ts
  22. 223 168
      src/lib/utils/cost-calculation.ts
  23. 34 12
      src/lib/utils/price-data.ts
  24. 405 0
      src/lib/utils/pricing-resolution.ts
  25. 69 4
      src/lib/utils/special-settings.ts
  26. 64 2
      src/repository/model-price.ts
  27. 18 1
      src/types/model-price.ts
  28. 28 1
      src/types/special-settings.ts
  29. 251 2
      tests/integration/billing-model-source.test.ts
  30. 60 0
      tests/unit/actions/model-prices.test.ts
  31. 46 0
      tests/unit/lib/cost-calculation-long-context.test.ts
  32. 46 0
      tests/unit/lib/cost-calculation-priority.test.ts
  33. 146 0
      tests/unit/lib/utils/pricing-resolution.test.ts
  34. 67 2
      tests/unit/lib/utils/special-settings.test.ts
  35. 32 4
      tests/unit/proxy/pricing-no-price.test.ts
  36. 34 3
      tests/unit/proxy/session.test.ts
  37. 81 0
      tests/unit/settings/prices/price-list-multi-provider-ui.test.tsx

+ 26 - 6
messages/en/dashboard.json

@@ -286,7 +286,17 @@
         "fast": "fast",
         "fastPriority": "Priority service tier (fast mode)",
         "context1m": "1M Context",
-        "context1mPricing": "Input 2x >200k, Output 1.5x >200k"
+        "context1mPricing": "Input 2x >200k, Output 1.5x >200k",
+        "pricingProvider": "Pricing provider",
+        "pricingSourceLabel": "Pricing source",
+        "pricingSource": {
+          "local_manual": "Local manual pricing",
+          "cloud_exact": "Exact cloud provider pricing",
+          "cloud_model_fallback": "Fallback model provider pricing",
+          "priority_fallback": "Priority provider fallback pricing",
+          "single_provider_top_level": "Top-level single-provider pricing",
+          "official_fallback": "Official fallback pricing"
+        }
       },
       "performance": {
         "title": "Performance",
@@ -359,12 +369,12 @@
       "targetModel": "Target Model"
     },
     "statusCodes": {
-      "not200": "Non-200 (errors/blocked)",
       "200": "200 (Success)",
       "400": "400 (Bad Request)",
       "401": "401 (Unauthorized)",
       "429": "429 (Rate Limited)",
-      "500": "500 (Server Error)"
+      "500": "500 (Server Error)",
+      "not200": "Non-200 (errors/blocked)"
     },
     "billingDetails": {
       "input": "Input",
@@ -379,7 +389,17 @@
       "fast": "fast",
       "fastPriority": "Priority service tier (fast mode)",
       "context1m": "1M Context",
-      "context1mPricing": "Input 2x >200k, Output 1.5x >200k"
+      "context1mPricing": "Input 2x >200k, Output 1.5x >200k",
+      "pricingProvider": "Pricing provider",
+      "pricingSourceLabel": "Pricing source",
+      "pricingSource": {
+        "local_manual": "Local manual pricing",
+        "cloud_exact": "Exact cloud provider pricing",
+        "cloud_model_fallback": "Fallback model provider pricing",
+        "priority_fallback": "Priority provider fallback pricing",
+        "single_provider_top_level": "Top-level single-provider pricing",
+        "official_fallback": "Official fallback pricing"
+      }
     }
   },
   "leaderboard": {
@@ -1685,11 +1705,11 @@
         "rolling": "Rolling window (24h)"
       },
       "quickValues": {
-        "unlimited": "Unlimited",
         "10": "$10",
         "50": "$50",
         "100": "$100",
-        "500": "$500"
+        "500": "$500",
+        "unlimited": "Unlimited"
       },
       "alreadySet": "Configured",
       "confirmAdd": "Add"

+ 16 - 2
messages/en/settings/prices.json

@@ -31,7 +31,8 @@
     "openrouter": "OpenRouter"
   },
   "badges": {
-    "local": "Local"
+    "local": "Local",
+    "multi": "Multi"
   },
   "capabilities": {
     "assistantPrefill": "Assistant prefill",
@@ -196,7 +197,8 @@
   "actions": {
     "edit": "Edit",
     "more": "More actions",
-    "delete": "Delete"
+    "delete": "Delete",
+    "comparePricing": "Compare pricing"
   },
   "toast": {
     "createSuccess": "Model added",
@@ -204,5 +206,17 @@
     "deleteSuccess": "Model deleted",
     "saveFailed": "Failed to save",
     "deleteFailed": "Failed to delete"
+  },
+  "providerPricing": {
+    "title": "Provider pricing · {model}",
+    "description": "Compare provider-specific cloud prices and pin one as a local model price.",
+    "input": "Input",
+    "output": "Output",
+    "cacheRead": "Cache Read",
+    "pinAction": "Pin as local price",
+    "pinSuccess": "Pinned {provider} pricing as local model price",
+    "pinFailed": "Failed to pin provider pricing",
+    "pinned": "Pinned",
+    "priority": "Priority"
   }
 }

+ 26 - 6
messages/ja/dashboard.json

@@ -286,7 +286,17 @@
         "fast": "fast",
         "fastPriority": "Priority service tier (fast モード)",
         "context1m": "1M コンテキスト",
-        "context1mPricing": "入力 2x >200k, 出力 1.5x >200k"
+        "context1mPricing": "入力 2x >200k, 出力 1.5x >200k",
+        "pricingProvider": "課金価格プロバイダー",
+        "pricingSourceLabel": "課金価格ソース",
+        "pricingSource": {
+          "local_manual": "ローカル手動価格",
+          "cloud_exact": "クラウドの正確なプロバイダー価格",
+          "cloud_model_fallback": "フォールバックモデルのプロバイダー価格",
+          "priority_fallback": "優先フォールバック価格",
+          "single_provider_top_level": "トップレベル単一プロバイダー価格",
+          "official_fallback": "公式フォールバック価格"
+        }
       },
       "performance": {
         "title": "パフォーマンス",
@@ -359,12 +369,12 @@
       "targetModel": "ターゲットモデル"
     },
     "statusCodes": {
-      "not200": "非 200(エラー/ブロック)",
       "200": "200(成功)",
       "400": "400 (不正なリクエスト)",
       "401": "401 (未認証)",
       "429": "429 (レート制限)",
-      "500": "500 (サーバーエラー)"
+      "500": "500 (サーバーエラー)",
+      "not200": "非 200(エラー/ブロック)"
     },
     "billingDetails": {
       "input": "入力",
@@ -379,7 +389,17 @@
       "fast": "fast",
       "fastPriority": "Priority service tier (fast モード)",
       "context1m": "1M コンテキスト",
-      "context1mPricing": "入力 >200k 2倍, 出力 >200k 1.5倍"
+      "context1mPricing": "入力 >200k 2倍, 出力 >200k 1.5倍",
+      "pricingProvider": "課金価格プロバイダー",
+      "pricingSourceLabel": "課金価格ソース",
+      "pricingSource": {
+        "local_manual": "ローカル手動価格",
+        "cloud_exact": "クラウドの正確なプロバイダー価格",
+        "cloud_model_fallback": "フォールバックモデルのプロバイダー価格",
+        "priority_fallback": "優先フォールバック価格",
+        "single_provider_top_level": "トップレベル単一プロバイダー価格",
+        "official_fallback": "公式フォールバック価格"
+      }
     }
   },
   "leaderboard": {
@@ -1622,11 +1642,11 @@
         "rolling": "ローリングウィンドウ(24時間)"
       },
       "quickValues": {
-        "unlimited": "無制限",
         "10": "$10",
         "50": "$50",
         "100": "$100",
-        "500": "$500"
+        "500": "$500",
+        "unlimited": "無制限"
       },
       "alreadySet": "設定済み",
       "confirmAdd": "追加",

+ 14 - 1
messages/ja/settings/prices.json

@@ -196,7 +196,8 @@
   "actions": {
     "edit": "編集",
     "more": "その他の操作",
-    "delete": "削除"
+    "delete": "削除",
+    "comparePricing": "価格を比較"
   },
   "toast": {
     "createSuccess": "モデルを追加しました",
@@ -204,5 +205,17 @@
     "deleteSuccess": "モデルを削除しました",
     "saveFailed": "保存に失敗しました",
     "deleteFailed": "削除に失敗しました"
+  },
+  "providerPricing": {
+    "title": "プロバイダー価格 · {model}",
+    "description": "プロバイダー別のクラウド価格を比較し、そのうち一つをローカルモデル価格として固定します。",
+    "input": "入力",
+    "output": "出力",
+    "cacheRead": "キャッシュ読取",
+    "pinAction": "ローカル価格として固定",
+    "pinSuccess": "{provider} の価格をローカルモデル価格として固定しました",
+    "pinFailed": "プロバイダー価格の固定に失敗しました",
+    "pinned": "固定済み",
+    "priority": "priority"
   }
 }

+ 26 - 6
messages/ru/dashboard.json

@@ -286,7 +286,17 @@
         "fast": "fast",
         "fastPriority": "Приоритетный уровень обслуживания (режим fast)",
         "context1m": "1M контекст",
-        "context1mPricing": "Вход 2x >200k, Выход 1.5x >200k"
+        "context1mPricing": "Вход 2x >200k, Выход 1.5x >200k",
+        "pricingProvider": "Провайдер тарифа",
+        "pricingSourceLabel": "Источник тарифа",
+        "pricingSource": {
+          "local_manual": "Локальная ручная цена",
+          "cloud_exact": "Точная облачная цена провайдера",
+          "cloud_model_fallback": "Цена провайдера из резервной модели",
+          "priority_fallback": "Приоритетная резервная цена",
+          "single_provider_top_level": "Цена верхнего уровня для одного провайдера",
+          "official_fallback": "Официальная резервная цена"
+        }
       },
       "performance": {
         "title": "Производительность",
@@ -359,12 +369,12 @@
       "targetModel": "Целевая модель"
     },
     "statusCodes": {
-      "not200": "Не 200 (ошибки/блокировки)",
       "200": "200 (Успех)",
       "400": "400 (Неверный запрос)",
       "401": "401 (Не авторизован)",
       "429": "429 (Ограничение)",
-      "500": "500 (Ошибка сервера)"
+      "500": "500 (Ошибка сервера)",
+      "not200": "Не 200 (ошибки/блокировки)"
     },
     "billingDetails": {
       "input": "Входные",
@@ -379,7 +389,17 @@
       "fast": "fast",
       "fastPriority": "Приоритетный уровень обслуживания (режим fast)",
       "context1m": "1M Контекст",
-      "context1mPricing": "Вход >200k 2x, Выход >200k 1.5x"
+      "context1mPricing": "Вход >200k 2x, Выход >200k 1.5x",
+      "pricingProvider": "Провайдер тарифа",
+      "pricingSourceLabel": "Источник тарифа",
+      "pricingSource": {
+        "local_manual": "Локальная ручная цена",
+        "cloud_exact": "Точная облачная цена провайдера",
+        "cloud_model_fallback": "Цена провайдера из резервной модели",
+        "priority_fallback": "Приоритетная резервная цена",
+        "single_provider_top_level": "Цена верхнего уровня для одного провайдера",
+        "official_fallback": "Официальная резервная цена"
+      }
     }
   },
   "leaderboard": {
@@ -1668,11 +1688,11 @@
         "rolling": "Скользящее окно (24ч)"
       },
       "quickValues": {
-        "unlimited": "Без ограничений",
         "10": "$10",
         "50": "$50",
         "100": "$100",
-        "500": "$500"
+        "500": "$500",
+        "unlimited": "Без ограничений"
       },
       "alreadySet": "Уже настроено",
       "confirmAdd": "Добавить"

+ 14 - 1
messages/ru/settings/prices.json

@@ -196,7 +196,8 @@
   "actions": {
     "edit": "Редактировать",
     "more": "Больше действий",
-    "delete": "Удалить"
+    "delete": "Удалить",
+    "comparePricing": "Сравнить цены"
   },
   "toast": {
     "createSuccess": "Модель добавлена",
@@ -204,5 +205,17 @@
     "deleteSuccess": "Модель удалена",
     "saveFailed": "Ошибка сохранения",
     "deleteFailed": "Ошибка удаления"
+  },
+  "providerPricing": {
+    "title": "Цены провайдеров · {model}",
+    "description": "Сравните облачные цены по провайдерам и закрепите одну из них как локальную цену модели.",
+    "input": "Вход",
+    "output": "Выход",
+    "cacheRead": "Чтение кэша",
+    "pinAction": "Закрепить как локальную цену",
+    "pinSuccess": "Цена {provider} закреплена как локальная цена модели",
+    "pinFailed": "Не удалось закрепить цену провайдера",
+    "pinned": "Закреплено",
+    "priority": "Priority"
   }
 }

+ 26 - 6
messages/zh-CN/dashboard.json

@@ -286,7 +286,17 @@
         "fast": "fast",
         "fastPriority": "优先服务等级(fast 模式)",
         "context1m": "1M 上下文",
-        "context1mPricing": "输入 2x >200k, 输出 1.5x >200k"
+        "context1mPricing": "输入 2x >200k, 输出 1.5x >200k",
+        "pricingProvider": "计费价格提供商",
+        "pricingSourceLabel": "计费价格来源",
+        "pricingSource": {
+          "local_manual": "本地手动价格",
+          "cloud_exact": "云端精确提供商价格",
+          "cloud_model_fallback": "回退模型的提供商价格",
+          "priority_fallback": "优先级回退价格",
+          "single_provider_top_level": "顶层单提供商价格",
+          "official_fallback": "官方兜底价格"
+        }
       },
       "performance": {
         "title": "性能数据",
@@ -359,12 +369,12 @@
       "targetModel": "目标模型"
     },
     "statusCodes": {
-      "not200": "非 200(全部非成功请求)",
       "200": "200 (成功)",
       "400": "400 (错误请求)",
       "401": "401 (未授权)",
       "429": "429 (限流)",
-      "500": "500 (服务器错误)"
+      "500": "500 (服务器错误)",
+      "not200": "非 200(全部非成功请求)"
     },
     "billingDetails": {
       "input": "输入",
@@ -379,7 +389,17 @@
       "fast": "fast",
       "fastPriority": "优先服务等级(fast 模式)",
       "context1m": "1M 上下文",
-      "context1mPricing": "输入 >200k 2倍, 输出 >200k 1.5倍"
+      "context1mPricing": "输入 >200k 2倍, 输出 >200k 1.5倍",
+      "pricingProvider": "计费价格提供商",
+      "pricingSourceLabel": "计费价格来源",
+      "pricingSource": {
+        "local_manual": "本地手动价格",
+        "cloud_exact": "云端精确提供商价格",
+        "cloud_model_fallback": "回退模型的提供商价格",
+        "priority_fallback": "优先级回退价格",
+        "single_provider_top_level": "顶层单提供商价格",
+        "official_fallback": "官方兜底价格"
+      }
     }
   },
   "leaderboard": {
@@ -1645,11 +1665,11 @@
         "rolling": "滚动窗口(24h)"
       },
       "quickValues": {
-        "unlimited": "无限",
         "10": "$10",
         "50": "$50",
         "100": "$100",
-        "500": "$500"
+        "500": "$500",
+        "unlimited": "无限"
       },
       "alreadySet": "已配置",
       "confirmAdd": "添加",

+ 16 - 2
messages/zh-CN/settings/prices.json

@@ -31,7 +31,8 @@
     "openrouter": "OpenRouter"
   },
   "badges": {
-    "local": "本地"
+    "local": "本地",
+    "multi": "多供应商"
   },
   "capabilities": {
     "assistantPrefill": "助手预填充",
@@ -196,7 +197,8 @@
   "actions": {
     "edit": "编辑",
     "more": "更多操作",
-    "delete": "删除"
+    "delete": "删除",
+    "comparePricing": "对比价格"
   },
   "toast": {
     "createSuccess": "模型已添加",
@@ -204,5 +206,17 @@
     "deleteSuccess": "模型已删除",
     "saveFailed": "保存失败",
     "deleteFailed": "删除失败"
+  },
+  "providerPricing": {
+    "title": "多供应商价格 · {model}",
+    "description": "对比不同供应商的云端价格,并将其中一项固化为本地模型价格。",
+    "input": "输入",
+    "output": "输出",
+    "cacheRead": "缓存读取",
+    "pinAction": "固化为本地价格",
+    "pinSuccess": "已将 {provider} 价格固化为本地模型价格",
+    "pinFailed": "固化供应商价格失败",
+    "pinned": "已固化",
+    "priority": "优先"
   }
 }

+ 26 - 6
messages/zh-TW/dashboard.json

@@ -286,7 +286,17 @@
         "fast": "fast",
         "fastPriority": "優先服務層級(fast 模式)",
         "context1m": "1M 上下文",
-        "context1mPricing": "輸入 2x >200k, 輸出 1.5x >200k"
+        "context1mPricing": "輸入 2x >200k, 輸出 1.5x >200k",
+        "pricingProvider": "計費價格供應商",
+        "pricingSourceLabel": "計費價格來源",
+        "pricingSource": {
+          "local_manual": "本地手動價格",
+          "cloud_exact": "雲端精確供應商價格",
+          "cloud_model_fallback": "回退模型的供應商價格",
+          "priority_fallback": "優先級回退價格",
+          "single_provider_top_level": "頂層單供應商價格",
+          "official_fallback": "官方兜底價格"
+        }
       },
       "performance": {
         "title": "效能資料",
@@ -359,12 +369,12 @@
       "targetModel": "目標模型"
     },
     "statusCodes": {
-      "not200": "非 200(所有非成功請求)",
       "200": "200(成功)",
       "400": "400(錯誤請求)",
       "401": "401(未授權)",
       "429": "429(限流)",
-      "500": "500(服務器錯誤)"
+      "500": "500(服務器錯誤)",
+      "not200": "非 200(所有非成功請求)"
     },
     "billingDetails": {
       "input": "輸入",
@@ -379,7 +389,17 @@
       "fast": "fast",
       "fastPriority": "優先服務層級(fast 模式)",
       "context1m": "1M 上下文長度",
-      "context1mPricing": "輸入 >200k 2倍, 輸出 >200k 1.5倍"
+      "context1mPricing": "輸入 >200k 2倍, 輸出 >200k 1.5倍",
+      "pricingProvider": "計費價格供應商",
+      "pricingSourceLabel": "計費價格來源",
+      "pricingSource": {
+        "local_manual": "本地手動價格",
+        "cloud_exact": "雲端精確供應商價格",
+        "cloud_model_fallback": "回退模型的供應商價格",
+        "priority_fallback": "優先級回退價格",
+        "single_provider_top_level": "頂層單供應商價格",
+        "official_fallback": "官方兜底價格"
+      }
     }
   },
   "leaderboard": {
@@ -1630,11 +1650,11 @@
         "rolling": "滾動視窗(24h)"
       },
       "quickValues": {
-        "unlimited": "無限",
         "10": "$10",
         "50": "$50",
         "100": "$100",
-        "500": "$500"
+        "500": "$500",
+        "unlimited": "無限"
       },
       "alreadySet": "已設定",
       "confirmAdd": "新增",

+ 14 - 1
messages/zh-TW/settings/prices.json

@@ -196,7 +196,8 @@
   "actions": {
     "edit": "編輯",
     "more": "更多動作",
-    "delete": "刪除"
+    "delete": "刪除",
+    "comparePricing": "比較價格"
   },
   "toast": {
     "createSuccess": "模型已新增",
@@ -204,5 +205,17 @@
     "deleteSuccess": "模型已刪除",
     "saveFailed": "儲存失敗",
     "deleteFailed": "刪除失敗"
+  },
+  "providerPricing": {
+    "title": "多供應商價格 · {model}",
+    "description": "比較不同供應商的雲端價格,並將其中一項固化為本地模型價格。",
+    "input": "輸入",
+    "output": "輸出",
+    "cacheRead": "快取讀取",
+    "pinAction": "固化為本地價格",
+    "pinSuccess": "已將 {provider} 價格固化為本地模型價格",
+    "pinFailed": "固化供應商價格失敗",
+    "pinned": "已固化",
+    "priority": "優先"
   }
 }

+ 78 - 1
src/actions/model-prices.ts

@@ -13,6 +13,7 @@ import {
   findAllLatestPrices,
   findAllLatestPricesPaginated,
   findAllManualPrices,
+  findLatestPriceByModelAndSource,
   hasAnyPriceRecords,
   type PaginatedResult,
   type PaginationParams,
@@ -66,6 +67,32 @@ function isPriceDataEqual(data1: ModelPriceData, data2: ModelPriceData): boolean
   return stableStringify(data1) === stableStringify(data2);
 }
 
+function buildManualPriceDataFromProviderPricing(
+  modelName: string,
+  basePriceData: ModelPriceData,
+  pricingProviderKey: string
+): ModelPriceData | null {
+  const pricing = basePriceData.pricing;
+  if (!pricing || typeof pricing !== "object" || Array.isArray(pricing)) {
+    return null;
+  }
+
+  const pricingNode = pricing[pricingProviderKey];
+  if (!pricingNode || typeof pricingNode !== "object" || Array.isArray(pricingNode)) {
+    return null;
+  }
+
+  return {
+    ...basePriceData,
+    ...pricingNode,
+    pricing: undefined,
+    litellm_provider: pricingProviderKey,
+    selected_pricing_provider: pricingProviderKey,
+    selected_pricing_source_model: modelName,
+    selected_pricing_resolution: "manual_pin",
+  };
+}
+
 /**
  * 价格表处理核心逻辑(内部函数,无权限检查)
  * 用于系统初始化和 Web UI 上传
@@ -587,7 +614,6 @@ export async function deleteSingleModelPrice(modelName: string): Promise<ActionR
     try {
       revalidatePath("/settings/prices");
     } catch (error) {
-      // 在后台任务/启动阶段可能没有 Next.js 的请求上下文,此处允许降级
       logger.debug("[ModelPrices] revalidatePath skipped", {
         error: error instanceof Error ? error.message : String(error),
       });
@@ -600,3 +626,54 @@ export async function deleteSingleModelPrice(modelName: string): Promise<ActionR
     return { ok: false, error: message };
   }
 }
+
+export async function pinModelPricingProviderAsManual(input: {
+  modelName: string;
+  pricingProviderKey: string;
+}): Promise<ActionResult<ModelPrice>> {
+  try {
+    const session = await getSession();
+    if (!session || session.user.role !== "admin") {
+      return { ok: false, error: "无权限执行此操作" };
+    }
+
+    const modelName = input.modelName?.trim();
+    const pricingProviderKey = input.pricingProviderKey?.trim();
+    if (!modelName) {
+      return { ok: false, error: "模型名称不能为空" };
+    }
+    if (!pricingProviderKey) {
+      return { ok: false, error: "价格提供商不能为空" };
+    }
+
+    const latestCloudPrice = await findLatestPriceByModelAndSource(modelName, "litellm");
+    if (!latestCloudPrice) {
+      return { ok: false, error: "未找到云端模型价格" };
+    }
+
+    const manualPriceData = buildManualPriceDataFromProviderPricing(
+      modelName,
+      latestCloudPrice.priceData,
+      pricingProviderKey
+    );
+    if (!manualPriceData) {
+      return { ok: false, error: "未找到对应的多供应商价格节点" };
+    }
+
+    const result = await upsertModelPrice(modelName, manualPriceData);
+
+    try {
+      revalidatePath("/settings/prices");
+    } catch (error) {
+      logger.debug("[ModelPrices] revalidatePath skipped", {
+        error: error instanceof Error ? error.message : String(error),
+      });
+    }
+
+    return { ok: true, data: result };
+  } catch (error) {
+    logger.error("固化多供应商价格失败:", error);
+    const message = error instanceof Error ? error.message : "操作失败,请稍后重试";
+    return { ok: false, error: message };
+  }
+}

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

@@ -20,6 +20,10 @@ import { Link } from "@/i18n/routing";
 import { cn, formatTokenAmount } from "@/lib/utils";
 import { formatCurrency } from "@/lib/utils/currency";
 import { formatProviderTimeline } from "@/lib/utils/provider-chain-formatter";
+import {
+  getPricingResolutionSpecialSetting,
+  hasPriorityServiceTierSpecialSetting,
+} from "@/lib/utils/special-settings";
 import type { MetadataTabProps } from "../types";
 
 export function MetadataTab({
@@ -49,6 +53,11 @@ export function MetadataTab({
 
   const specialSettingsContent =
     specialSettings && specialSettings.length > 0 ? JSON.stringify(specialSettings, null, 2) : null;
+  const pricingResolution = getPricingResolutionSpecialSetting(specialSettings);
+  const pricingSourceLabel = pricingResolution
+    ? t(`billingDetails.pricingSource.${pricingResolution.source}`)
+    : null;
+  const hasPriorityServiceTier = hasPriorityServiceTierSpecialSetting(specialSettings);
 
   const handleCopyTimeline = () => {
     if (!providerChain) return;
@@ -240,6 +249,37 @@ export function MetadataTab({
                 </div>
               )}
 
+              {hasPriorityServiceTier ? (
+                <div className="flex justify-between items-center col-span-2">
+                  <span className="text-muted-foreground">{t("billingDetails.fast")}:</span>
+                  <Badge
+                    variant="outline"
+                    className="text-xs bg-orange-50 text-orange-700 border-orange-200 dark:bg-orange-950/30 dark:text-orange-300 dark:border-orange-800"
+                  >
+                    {t("billingDetails.fastPriority")}
+                  </Badge>
+                </div>
+              ) : null}
+
+              {pricingResolution && pricingSourceLabel ? (
+                <>
+                  <div className="flex justify-between col-span-2">
+                    <span className="text-muted-foreground">
+                      {t("billingDetails.pricingProvider")}:
+                    </span>
+                    <span className="font-mono">
+                      {pricingResolution.resolvedPricingProviderKey}
+                    </span>
+                  </div>
+                  <div className="flex justify-between col-span-2">
+                    <span className="text-muted-foreground">
+                      {t("billingDetails.pricingSourceLabel")}:
+                    </span>
+                    <span>{pricingSourceLabel}</span>
+                  </div>
+                </>
+              ) : null}
+
               {/* Cost Multiplier */}
               {(() => {
                 if (costMultiplier === "" || costMultiplier == null) return null;

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

@@ -20,6 +20,10 @@ import { Button } from "@/components/ui/button";
 import { Link } from "@/i18n/routing";
 import { cn, formatTokenAmount } from "@/lib/utils";
 import { formatCurrency } from "@/lib/utils/currency";
+import {
+  getPricingResolutionSpecialSetting,
+  hasPriorityServiceTierSpecialSetting,
+} from "@/lib/utils/special-settings";
 import { getFake200ReasonKey } from "../../fake200-reason";
 import {
   calculateOutputRate,
@@ -67,6 +71,11 @@ export function SummaryTab({
   const hasRedirect = originalModel && currentModel && originalModel !== currentModel;
   const specialSettingsContent =
     specialSettings && specialSettings.length > 0 ? JSON.stringify(specialSettings, null, 2) : null;
+  const pricingResolution = getPricingResolutionSpecialSetting(specialSettings);
+  const pricingSourceLabel = pricingResolution
+    ? t(`billingDetails.pricingSource.${pricingResolution.source}`)
+    : null;
+  const hasPriorityServiceTier = hasPriorityServiceTierSpecialSetting(specialSettings);
   const isFake200PostStreamFailure =
     typeof errorMessage === "string" && errorMessage.startsWith("FAKE_200_");
   const fake200Code =
@@ -353,6 +362,37 @@ export function SummaryTab({
                 </div>
               )}
 
+              {hasPriorityServiceTier ? (
+                <div className="flex justify-between items-center col-span-2">
+                  <span className="text-muted-foreground">{t("billingDetails.fast")}:</span>
+                  <Badge
+                    variant="outline"
+                    className="text-xs bg-orange-50 text-orange-700 border-orange-200 dark:bg-orange-950/30 dark:text-orange-300 dark:border-orange-800"
+                  >
+                    {t("billingDetails.fastPriority")}
+                  </Badge>
+                </div>
+              ) : null}
+
+              {pricingResolution && pricingSourceLabel ? (
+                <>
+                  <div className="flex justify-between col-span-2">
+                    <span className="text-muted-foreground">
+                      {t("billingDetails.pricingProvider")}:
+                    </span>
+                    <span className="font-mono">
+                      {pricingResolution.resolvedPricingProviderKey}
+                    </span>
+                  </div>
+                  <div className="flex justify-between col-span-2">
+                    <span className="text-muted-foreground">
+                      {t("billingDetails.pricingSourceLabel")}:
+                    </span>
+                    <span>{pricingSourceLabel}</span>
+                  </div>
+                </>
+              ) : null}
+
               {/* Cost Multiplier */}
               {(() => {
                 if (costMultiplier === "" || costMultiplier == null) return null;

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

@@ -348,3 +348,37 @@ describe("usage-logs-table multiplier badge", () => {
     container.remove();
   });
 });
+
+describe("usage-logs-table pricing resolution", () => {
+  test("renders pricing provider and source details when pricing_resolution special setting exists", () => {
+    const html = renderToStaticMarkup(
+      <UsageLogsTable
+        logs={[
+          makeLog({
+            id: 1,
+            specialSettings: [
+              {
+                type: "pricing_resolution",
+                scope: "billing",
+                hit: true,
+                modelName: "gpt-5.4",
+                resolvedModelName: "gpt-5.4",
+                resolvedPricingProviderKey: "openai",
+                source: "priority_fallback",
+              },
+            ],
+          }),
+        ]}
+        total={1}
+        page={1}
+        pageSize={50}
+        onPageChange={() => {}}
+        isPending={false}
+      />
+    );
+
+    expect(html).toContain("logs.billingDetails.pricingProvider");
+    expect(html).toContain("openai");
+    expect(html).toContain("logs.billingDetails.pricingSource.priority_fallback");
+  });
+});

+ 18 - 1
src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx

@@ -26,7 +26,10 @@ import {
   shouldHideOutputRate,
 } from "@/lib/utils/performance-formatter";
 import { formatProviderSummary } from "@/lib/utils/provider-chain-formatter";
-import { hasPriorityServiceTierSpecialSetting } from "@/lib/utils/special-settings";
+import {
+  getPricingResolutionSpecialSetting,
+  hasPriorityServiceTierSpecialSetting,
+} from "@/lib/utils/special-settings";
 import type { UsageLogRow } from "@/repository/usage-logs";
 import type { BillingModelSource } from "@/types/system-config";
 import { ErrorDetailsDialog } from "./error-details-dialog";
@@ -59,6 +62,8 @@ export function UsageLogsTable({
   const t = useTranslations("dashboard");
   const tChain = useTranslations("provider-chain");
   const totalPages = Math.ceil(total / pageSize);
+  const getPricingSourceLabel = (source: string) =>
+    t(`logs.billingDetails.pricingSource.${source}`);
 
   // 弹窗状态管理:记录当前打开的行 ID 和是否需要滚动到重定向部分
   const [dialogState, setDialogState] = useState<{
@@ -111,6 +116,7 @@ export function UsageLogsTable({
                 const isNonBilling = log.endpoint === NON_BILLING_ENDPOINT;
                 const isWarmupSkipped = log.blockedBy === "warmup";
                 const isMutedRow = isNonBilling || isWarmupSkipped;
+                const pricingResolution = getPricingResolutionSpecialSetting(log.specialSettings);
 
                 // 计算倍率(用于 Provider 列 Badge 和成本明细)
                 const successfulProvider =
@@ -412,6 +418,17 @@ export function UsageLogsTable({
                                   {t("logs.billingDetails.context1m")}
                                 </div>
                               )}
+                              {pricingResolution && (
+                                <>
+                                  <div>
+                                    {t("logs.billingDetails.pricingProvider")}:{" "}
+                                    <span className="font-mono">
+                                      {pricingResolution.resolvedPricingProviderKey}
+                                    </span>
+                                  </div>
+                                  <div>{getPricingSourceLabel(pricingResolution.source)}</div>
+                                </>
+                              )}
                               <div>
                                 {t("logs.billingDetails.input")}:{" "}
                                 {formatTokenAmount(log.inputTokens)} tokens

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

@@ -22,7 +22,10 @@ import {
   NON_BILLING_ENDPOINT,
   shouldHideOutputRate,
 } from "@/lib/utils/performance-formatter";
-import { hasPriorityServiceTierSpecialSetting } from "@/lib/utils/special-settings";
+import {
+  getPricingResolutionSpecialSetting,
+  hasPriorityServiceTierSpecialSetting,
+} from "@/lib/utils/special-settings";
 import type { ProviderChainItem } from "@/types/message";
 import type { BillingModelSource } from "@/types/system-config";
 import { ErrorDetailsDialog } from "./error-details-dialog";
@@ -56,6 +59,7 @@ interface VirtualizedLogsTableProps {
   hideScrollToTop?: boolean;
   hiddenColumns?: LogsTableColumn[];
   bodyClassName?: string;
+  serverTimeZone?: string;
 }
 
 export function VirtualizedLogsTable({
@@ -68,8 +72,10 @@ export function VirtualizedLogsTable({
   hideScrollToTop = false,
   hiddenColumns,
   bodyClassName,
+  serverTimeZone: _serverTimeZone,
 }: VirtualizedLogsTableProps) {
   const t = useTranslations("dashboard");
+  const getPricingSourceLabel = (source: string) => t(`logs.billingDetails.pricingSource.`);
   const tChain = useTranslations("provider-chain");
   const parentRef = useRef<HTMLDivElement>(null);
   const [showScrollToTop, setShowScrollToTop] = useState(false);
@@ -332,6 +338,8 @@ export function VirtualizedLogsTable({
                 }
 
                 const isNonBilling = log.endpoint === NON_BILLING_ENDPOINT;
+                const _isWarmupSkipped = log.blockedBy === "warmup";
+                const pricingResolution = getPricingResolutionSpecialSetting(log.specialSettings);
 
                 return (
                   <div
@@ -665,6 +673,17 @@ export function VirtualizedLogsTable({
                                     {t("logs.billingDetails.context1m")}
                                   </div>
                                 )}
+                                {pricingResolution && (
+                                  <>
+                                    <div>
+                                      {t("logs.billingDetails.pricingProvider")}:{" "}
+                                      <span className="font-mono">
+                                        {pricingResolution.resolvedPricingProviderKey}
+                                      </span>
+                                    </div>
+                                    <div>{getPricingSourceLabel(pricingResolution.source)}</div>
+                                  </>
+                                )}
                                 <div>
                                   {t("logs.billingDetails.input")}:{" "}
                                   {formatTokenAmount(log.inputTokens)} tokens

+ 225 - 182
src/app/[locale]/settings/prices/_components/price-list.tsx

@@ -2,6 +2,7 @@
 
 import { formatInTimeZone } from "date-fns-tz";
 import {
+  ArrowRightLeft,
   Braces,
   ChevronLeft,
   ChevronRight,
@@ -42,9 +43,11 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
 import { useDebounce } from "@/lib/hooks/use-debounce";
 import { PRICE_FILTER_VENDORS } from "@/lib/model-vendor-icons";
 import { copyToClipboard } from "@/lib/utils/clipboard";
+import { resolvePricingForModelRecords } from "@/lib/utils/pricing-resolution";
 import type { ModelPrice, ModelPriceSource } from "@/types/model-price";
 import { DeleteModelDialog } from "./delete-model-dialog";
 import { ModelPriceDrawer } from "./model-price-drawer";
+import { ProviderPricingDialog } from "./provider-pricing-dialog";
 
 interface PriceListProps {
   initialPrices: ModelPrice[];
@@ -445,206 +448,246 @@ export function PriceList({
                 </td>
               </tr>
             ) : filteredPrices.length > 0 ? (
-              filteredPrices.map((price) => (
-                <tr
-                  key={price.id}
-                  className="border-b border-white/5 hover:bg-white/[0.02] transition-colors"
-                >
-                  <td className="py-3 px-4 text-sm text-foreground whitespace-normal break-words">
-                    <div className="flex flex-wrap items-center gap-2">
-                      <span className="font-medium">
-                        {price.priceData.display_name?.trim() || price.modelName}
-                      </span>
-                      {price.priceData.litellm_provider ? (
-                        <Badge variant="secondary" className="font-mono text-xs">
-                          {price.priceData.litellm_provider}
-                        </Badge>
-                      ) : null}
-                      {price.source === "manual" && (
-                        <Badge variant="outline">{t("badges.local")}</Badge>
-                      )}
-                    </div>
-                    <div className="mt-1">
-                      <Tooltip>
-                        <TooltipTrigger asChild>
-                          <button
-                            type="button"
-                            aria-label={t("table.copyModelId")}
-                            className="font-mono text-xs text-muted-foreground hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
-                            onClick={() => handleCopyModelId(price.modelName)}
-                          >
-                            {price.modelName}
-                          </button>
-                        </TooltipTrigger>
-                        <TooltipContent sideOffset={4}>{t("table.copyModelId")}</TooltipContent>
-                      </Tooltip>
-                    </div>
-                  </td>
-                  <td className="py-3 px-4 text-sm text-foreground">
-                    <div className="flex flex-wrap gap-1">
-                      {capabilityItems.map(({ key, icon: Icon, label }) => {
-                        const enabled = price.priceData[key] === true;
-                        const status = enabled
-                          ? t("capabilities.statusSupported")
-                          : t("capabilities.statusUnsupported");
-                        const tooltipText = t("capabilities.tooltip", { label, status });
-                        return (
-                          <Tooltip key={key}>
-                            <TooltipTrigger asChild>
-                              <button
-                                type="button"
-                                aria-label={tooltipText}
-                                className={`inline-flex h-7 w-7 items-center justify-center rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
-                                  enabled
-                                    ? "bg-[#E25706]/10 text-[#E25706] border-[#E25706]/20"
-                                    : "bg-muted/30 text-muted-foreground/40 border-transparent"
-                                }`}
-                              >
-                                <Icon className="h-4 w-4" aria-hidden="true" />
-                              </button>
-                            </TooltipTrigger>
-                            <TooltipContent sideOffset={4}>{tooltipText}</TooltipContent>
-                          </Tooltip>
-                        );
-                      })}
-                    </div>
-                  </td>
-                  <td className="py-3 px-4 font-mono text-sm text-right">
-                    <div className="space-y-1">
-                      <div className="flex items-center justify-end gap-2">
-                        <span className="text-xs text-muted-foreground">
-                          {t("table.priceInput")}
-                        </span>
-                        <span className="text-muted-foreground">
-                          {price.priceData.mode === "image_generation"
-                            ? "-"
-                            : formatPerMillionTokenPriceLabel(price.priceData.input_cost_per_token)}
+              filteredPrices.map((price) => {
+                const displayPricing = resolvePricingForModelRecords({
+                  provider: null,
+                  primaryModelName: price.modelName,
+                  fallbackModelName: null,
+                  primaryRecord: price,
+                  fallbackRecord: null,
+                });
+                const displayPriceData = displayPricing?.priceData ?? price.priceData;
+                const displayPricingProviderKey =
+                  displayPricing?.resolvedPricingProviderKey ??
+                  (typeof displayPriceData.selected_pricing_provider === "string"
+                    ? displayPriceData.selected_pricing_provider
+                    : null);
+
+                return (
+                  <tr
+                    key={price.id}
+                    className="border-b border-white/5 hover:bg-white/[0.02] transition-colors"
+                  >
+                    <td className="py-3 px-4 text-sm text-foreground whitespace-normal break-words">
+                      <div className="flex flex-wrap items-center gap-2">
+                        <span className="font-medium">
+                          {price.priceData.display_name?.trim() || price.modelName}
                         </span>
+                        {price.priceData.litellm_provider ? (
+                          <Badge variant="secondary" className="font-mono text-xs">
+                            {price.priceData.litellm_provider}
+                          </Badge>
+                        ) : null}
+                        {displayPricingProviderKey &&
+                        displayPricingProviderKey !== price.priceData.litellm_provider ? (
+                          <Badge variant="outline" className="font-mono text-xs">
+                            {displayPricingProviderKey}
+                          </Badge>
+                        ) : null}
+                        {price.priceData.pricing &&
+                        Object.keys(price.priceData.pricing).length > 1 ? (
+                          <Badge variant="outline">{t("badges.multi")}</Badge>
+                        ) : null}
+                        {price.source === "manual" && (
+                          <Badge variant="outline">{t("badges.local")}</Badge>
+                        )}
                       </div>
-                      <div className="flex items-center justify-end gap-2">
-                        <span className="text-xs text-muted-foreground">
-                          {t("table.priceOutput")}
-                        </span>
-                        <span className="text-muted-foreground">
-                          {price.priceData.mode === "image_generation"
-                            ? formatPerImagePriceLabel(price.priceData.output_cost_per_image)
-                            : formatPerMillionTokenPriceLabel(
-                                price.priceData.output_cost_per_token
-                              )}
-                        </span>
+                      <div className="mt-1">
+                        <Tooltip>
+                          <TooltipTrigger asChild>
+                            <button
+                              type="button"
+                              aria-label={t("table.copyModelId")}
+                              className="font-mono text-xs text-muted-foreground hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
+                              onClick={() => handleCopyModelId(price.modelName)}
+                            >
+                              {price.modelName}
+                            </button>
+                          </TooltipTrigger>
+                          <TooltipContent sideOffset={4}>{t("table.copyModelId")}</TooltipContent>
+                        </Tooltip>
                       </div>
-                      {formatPerRequestPriceLabel(price.priceData.input_cost_per_request) !==
-                      "-" ? (
-                        <div className="flex items-center justify-end gap-2">
-                          <span className="text-xs text-muted-foreground">
-                            {t("table.pricePerRequest")}
-                          </span>
-                          <span className="text-muted-foreground">
-                            {formatPerRequestPriceLabel(price.priceData.input_cost_per_request)}
-                          </span>
-                        </div>
-                      ) : null}
-                    </div>
-                  </td>
-                  <td className="py-3 px-4 font-mono text-sm text-right">
-                    <span className="text-muted-foreground">
-                      {price.priceData.supports_prompt_caching === true
-                        ? formatPerMillionTokenPriceLabel(
-                            price.priceData.cache_read_input_token_cost
-                          )
-                        : "-"}
-                    </span>
-                  </td>
-                  <td className="py-3 px-4 font-mono text-sm text-right">
-                    {price.priceData.supports_prompt_caching === true ? (
+                    </td>
+                    <td className="py-3 px-4 text-sm text-foreground">
+                      <div className="flex flex-wrap gap-1">
+                        {capabilityItems.map(({ key, icon: Icon, label }) => {
+                          const enabled = price.priceData[key] === true;
+                          const status = enabled
+                            ? t("capabilities.statusSupported")
+                            : t("capabilities.statusUnsupported");
+                          const tooltipText = t("capabilities.tooltip", { label, status });
+                          return (
+                            <Tooltip key={key}>
+                              <TooltipTrigger asChild>
+                                <button
+                                  type="button"
+                                  aria-label={tooltipText}
+                                  className={`inline-flex h-7 w-7 items-center justify-center rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
+                                    enabled
+                                      ? "bg-[#E25706]/10 text-[#E25706] border-[#E25706]/20"
+                                      : "bg-muted/30 text-muted-foreground/40 border-transparent"
+                                  }`}
+                                >
+                                  <Icon className="h-4 w-4" aria-hidden="true" />
+                                </button>
+                              </TooltipTrigger>
+                              <TooltipContent sideOffset={4}>{tooltipText}</TooltipContent>
+                            </Tooltip>
+                          );
+                        })}
+                      </div>
+                    </td>
+                    <td className="py-3 px-4 font-mono text-sm text-right">
                       <div className="space-y-1">
                         <div className="flex items-center justify-end gap-2">
                           <span className="text-xs text-muted-foreground">
-                            {t("table.cache5m")}
+                            {t("table.priceInput")}
                           </span>
                           <span className="text-muted-foreground">
-                            {formatPerMillionTokenPriceLabel(
-                              price.priceData.cache_creation_input_token_cost
-                            )}
+                            {displayPriceData.mode === "image_generation"
+                              ? "-"
+                              : formatPerMillionTokenPriceLabel(
+                                  displayPriceData.input_cost_per_token
+                                )}
                           </span>
                         </div>
                         <div className="flex items-center justify-end gap-2">
                           <span className="text-xs text-muted-foreground">
-                            {t("table.cache1h")}
+                            {t("table.priceOutput")}
                           </span>
                           <span className="text-muted-foreground">
-                            {formatPerMillionTokenPriceLabel(
-                              price.priceData.cache_creation_input_token_cost_above_1hr
-                            )}
+                            {displayPriceData.mode === "image_generation"
+                              ? formatPerImagePriceLabel(displayPriceData.output_cost_per_image)
+                              : formatPerMillionTokenPriceLabel(
+                                  displayPriceData.output_cost_per_token
+                                )}
                           </span>
                         </div>
+                        {formatPerRequestPriceLabel(displayPriceData.input_cost_per_request) !==
+                        "-" ? (
+                          <div className="flex items-center justify-end gap-2">
+                            <span className="text-xs text-muted-foreground">
+                              {t("table.pricePerRequest")}
+                            </span>
+                            <span className="text-muted-foreground">
+                              {formatPerRequestPriceLabel(displayPriceData.input_cost_per_request)}
+                            </span>
+                          </div>
+                        ) : null}
                       </div>
-                    ) : (
-                      <span className="text-muted-foreground">-</span>
-                    )}
-                  </td>
-                  <td className="py-3 px-4 text-sm text-muted-foreground">
-                    {formatInTimeZone(
-                      new Date(price.updatedAt ?? price.createdAt),
-                      timeZone,
-                      "yyyy-MM-dd"
-                    )}
-                  </td>
-                  <td className="py-3 px-4">
-                    <DropdownMenu>
-                      <DropdownMenuTrigger asChild>
-                        <Button
-                          variant="ghost"
-                          size="icon"
-                          className="h-8 w-8"
-                          aria-label={t("actions.more")}
-                        >
-                          <MoreHorizontal className="h-4 w-4" />
-                        </Button>
-                      </DropdownMenuTrigger>
-                      <DropdownMenuContent align="end">
-                        <ModelPriceDrawer
-                          mode="edit"
-                          initialData={price}
-                          trigger={
-                            <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
-                              <Pencil className="h-4 w-4 mr-2" />
-                              {t("actions.edit")}
-                            </DropdownMenuItem>
-                          }
-                        />
-                        <DeleteModelDialog
-                          modelName={price.modelName}
-                          onSuccess={() => {
-                            const willBeEmpty = filteredPrices.length <= 1 && page > 1;
-                            const targetPage = willBeEmpty ? page - 1 : page;
-                            if (targetPage !== page) {
-                              pendingRefreshPage.current = targetPage;
-                              setPage(targetPage);
-                              updateURL(
-                                debouncedSearchTerm,
-                                targetPage,
-                                pageSize,
-                                sourceFilter,
-                                litellmProviderFilter
-                              );
+                    </td>
+                    <td className="py-3 px-4 font-mono text-sm text-right">
+                      <span className="text-muted-foreground">
+                        {displayPriceData.supports_prompt_caching === true
+                          ? formatPerMillionTokenPriceLabel(
+                              displayPriceData.cache_read_input_token_cost
+                            )
+                          : "-"}
+                      </span>
+                    </td>
+                    <td className="py-3 px-4 font-mono text-sm text-right">
+                      {displayPriceData.supports_prompt_caching === true ? (
+                        <div className="space-y-1">
+                          <div className="flex items-center justify-end gap-2">
+                            <span className="text-xs text-muted-foreground">
+                              {t("table.cache5m")}
+                            </span>
+                            <span className="text-muted-foreground">
+                              {formatPerMillionTokenPriceLabel(
+                                displayPriceData.cache_creation_input_token_cost
+                              )}
+                            </span>
+                          </div>
+                          <div className="flex items-center justify-end gap-2">
+                            <span className="text-xs text-muted-foreground">
+                              {t("table.cache1h")}
+                            </span>
+                            <span className="text-muted-foreground">
+                              {formatPerMillionTokenPriceLabel(
+                                displayPriceData.cache_creation_input_token_cost_above_1hr
+                              )}
+                            </span>
+                          </div>
+                        </div>
+                      ) : (
+                        <span className="text-muted-foreground">-</span>
+                      )}
+                    </td>
+                    <td className="py-3 px-4 text-sm text-muted-foreground">
+                      {formatInTimeZone(
+                        new Date(price.updatedAt ?? price.createdAt),
+                        timeZone,
+                        "yyyy-MM-dd"
+                      )}
+                    </td>
+                    <td className="py-3 px-4">
+                      <DropdownMenu>
+                        <DropdownMenuTrigger asChild>
+                          <Button
+                            variant="ghost"
+                            size="icon"
+                            className="h-8 w-8"
+                            aria-label={t("actions.more")}
+                          >
+                            <MoreHorizontal className="h-4 w-4" />
+                          </Button>
+                        </DropdownMenuTrigger>
+                        <DropdownMenuContent align="end">
+                          {price.priceData.pricing &&
+                          Object.keys(price.priceData.pricing).length > 0 ? (
+                            <ProviderPricingDialog
+                              price={price}
+                              trigger={
+                                <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
+                                  <ArrowRightLeft className="h-4 w-4 mr-2" />
+                                  {t("actions.comparePricing")}
+                                </DropdownMenuItem>
+                              }
+                            />
+                          ) : null}
+                          <ModelPriceDrawer
+                            mode="edit"
+                            initialData={price}
+                            trigger={
+                              <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
+                                <Pencil className="h-4 w-4 mr-2" />
+                                {t("actions.edit")}
+                              </DropdownMenuItem>
                             }
-                          }}
-                          trigger={
-                            <DropdownMenuItem
-                              onSelect={(e) => e.preventDefault()}
-                              className="text-destructive focus:text-destructive"
-                            >
-                              <Trash2 className="h-4 w-4 mr-2" />
-                              {t("actions.delete")}
-                            </DropdownMenuItem>
-                          }
-                        />
-                      </DropdownMenuContent>
-                    </DropdownMenu>
-                  </td>
-                </tr>
-              ))
+                          />
+                          <DeleteModelDialog
+                            modelName={price.modelName}
+                            onSuccess={() => {
+                              const willBeEmpty = filteredPrices.length <= 1 && page > 1;
+                              const targetPage = willBeEmpty ? page - 1 : page;
+                              if (targetPage !== page) {
+                                pendingRefreshPage.current = targetPage;
+                                setPage(targetPage);
+                                updateURL(
+                                  debouncedSearchTerm,
+                                  targetPage,
+                                  pageSize,
+                                  sourceFilter,
+                                  litellmProviderFilter
+                                );
+                              }
+                            }}
+                            trigger={
+                              <DropdownMenuItem
+                                onSelect={(e) => e.preventDefault()}
+                                className="text-destructive focus:text-destructive"
+                              >
+                                <Trash2 className="h-4 w-4 mr-2" />
+                                {t("actions.delete")}
+                              </DropdownMenuItem>
+                            }
+                          />
+                        </DropdownMenuContent>
+                      </DropdownMenu>
+                    </td>
+                  </tr>
+                );
+              })
             ) : (
               <tr>
                 <td colSpan={7} className="text-center py-8">

+ 197 - 0
src/app/[locale]/settings/prices/_components/provider-pricing-dialog.tsx

@@ -0,0 +1,197 @@
+"use client";
+
+import { ArrowRightLeft, Loader2, Pin } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useMemo, useState } from "react";
+import { toast } from "sonner";
+import { pinModelPricingProviderAsManual } from "@/actions/model-prices";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogHeader,
+  DialogTitle,
+  DialogTrigger,
+} from "@/components/ui/dialog";
+import type { ModelPrice } from "@/types/model-price";
+
+interface ProviderPricingDialogProps {
+  price: ModelPrice;
+  trigger?: React.ReactNode;
+  onSuccess?: () => void;
+}
+
+function formatScalar(value?: number): string {
+  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
+    return "-";
+  }
+  if (value < 0.01) return value.toFixed(4);
+  if (value < 1) return value.toFixed(3);
+  if (value < 100) return value.toFixed(2);
+  return value.toFixed(0);
+}
+
+function formatTokenPrice(value?: number): string {
+  const formatted = formatScalar(typeof value === "number" ? value * 1000000 : undefined);
+  return formatted === "-" ? "-" : `$${formatted}/M`;
+}
+
+export function ProviderPricingDialog({ price, trigger, onSuccess }: ProviderPricingDialogProps) {
+  const t = useTranslations("settings.prices");
+  const tCommon = useTranslations("common");
+  const [open, setOpen] = useState(false);
+  const [pinningKey, setPinningKey] = useState<string | null>(null);
+
+  const pricingEntries = useMemo(() => {
+    const pricing = price.priceData.pricing;
+    if (!pricing || typeof pricing !== "object" || Array.isArray(pricing)) {
+      return [] as Array<[string, Record<string, unknown>]>;
+    }
+
+    return Object.entries(pricing)
+      .filter((entry): entry is [string, Record<string, unknown>] => {
+        return !!entry[1] && typeof entry[1] === "object" && !Array.isArray(entry[1]);
+      })
+      .sort((a, b) => a[0].localeCompare(b[0]));
+  }, [price.priceData.pricing]);
+
+  const handlePin = async (pricingProviderKey: string) => {
+    setPinningKey(pricingProviderKey);
+    try {
+      const result = await pinModelPricingProviderAsManual({
+        modelName: price.modelName,
+        pricingProviderKey,
+      });
+      if (!result.ok) {
+        toast.error(result.error);
+        return;
+      }
+
+      toast.success(t("providerPricing.pinSuccess", { provider: pricingProviderKey }));
+      setOpen(false);
+      onSuccess?.();
+      window.dispatchEvent(new Event("price-data-updated"));
+    } catch (error) {
+      console.error("pin provider pricing failed", error);
+      toast.error(t("providerPricing.pinFailed"));
+    } finally {
+      setPinningKey(null);
+    }
+  };
+
+  if (pricingEntries.length === 0) {
+    return null;
+  }
+
+  const defaultTrigger = (
+    <Button variant="ghost" size="sm">
+      <ArrowRightLeft className="h-4 w-4 mr-2" />
+      {t("actions.comparePricing")}
+    </Button>
+  );
+
+  return (
+    <Dialog open={open} onOpenChange={setOpen}>
+      <DialogTrigger asChild>{trigger || defaultTrigger}</DialogTrigger>
+      <DialogContent className="max-w-3xl">
+        <DialogHeader>
+          <DialogTitle>{t("providerPricing.title", { model: price.modelName })}</DialogTitle>
+          <DialogDescription>{t("providerPricing.description")}</DialogDescription>
+        </DialogHeader>
+
+        <div className="space-y-3 max-h-[70vh] overflow-y-auto pr-1">
+          {pricingEntries.map(([providerKey, providerPricing]) => {
+            const isPinning = pinningKey === providerKey;
+            return (
+              <div
+                key={providerKey}
+                className="rounded-lg border border-border bg-card p-4 space-y-3"
+              >
+                <div className="flex items-center justify-between gap-3">
+                  <div className="flex items-center gap-2">
+                    <Badge variant="secondary" className="font-mono text-xs">
+                      {providerKey}
+                    </Badge>
+                    {price.priceData.selected_pricing_provider === providerKey ? (
+                      <Badge variant="outline">{t("providerPricing.pinned")}</Badge>
+                    ) : null}
+                  </div>
+                  <Button
+                    size="sm"
+                    variant="outline"
+                    disabled={isPinning}
+                    onClick={() => void handlePin(providerKey)}
+                  >
+                    {isPinning ? (
+                      <Loader2 className="h-4 w-4 mr-2 animate-spin" />
+                    ) : (
+                      <Pin className="h-4 w-4 mr-2" />
+                    )}
+                    {t("providerPricing.pinAction")}
+                  </Button>
+                </div>
+
+                <div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-sm">
+                  <div>
+                    <div className="text-muted-foreground">{t("providerPricing.input")}</div>
+                    <div className="font-mono">
+                      {formatTokenPrice(providerPricing.input_cost_per_token as number | undefined)}
+                    </div>
+                    {typeof providerPricing.input_cost_per_token_priority === "number" ? (
+                      <div className="font-mono text-xs text-orange-600 dark:text-orange-400">
+                        {t("providerPricing.priority")}:{" "}
+                        {formatTokenPrice(
+                          providerPricing.input_cost_per_token_priority as number | undefined
+                        )}
+                      </div>
+                    ) : null}
+                  </div>
+                  <div>
+                    <div className="text-muted-foreground">{t("providerPricing.output")}</div>
+                    <div className="font-mono">
+                      {formatTokenPrice(
+                        providerPricing.output_cost_per_token as number | undefined
+                      )}
+                    </div>
+                    {typeof providerPricing.output_cost_per_token_priority === "number" ? (
+                      <div className="font-mono text-xs text-orange-600 dark:text-orange-400">
+                        {t("providerPricing.priority")}:{" "}
+                        {formatTokenPrice(
+                          providerPricing.output_cost_per_token_priority as number | undefined
+                        )}
+                      </div>
+                    ) : null}
+                  </div>
+                  <div>
+                    <div className="text-muted-foreground">{t("providerPricing.cacheRead")}</div>
+                    <div className="font-mono">
+                      {formatTokenPrice(
+                        providerPricing.cache_read_input_token_cost as number | undefined
+                      )}
+                    </div>
+                    {typeof providerPricing.cache_read_input_token_cost_priority === "number" ? (
+                      <div className="font-mono text-xs text-orange-600 dark:text-orange-400">
+                        {t("providerPricing.priority")}:{" "}
+                        {formatTokenPrice(
+                          providerPricing.cache_read_input_token_cost_priority as number | undefined
+                        )}
+                      </div>
+                    ) : null}
+                  </div>
+                </div>
+              </div>
+            );
+          })}
+        </div>
+
+        <div className="flex justify-end">
+          <Button variant="ghost" onClick={() => setOpen(false)}>
+            {tCommon("close")}
+          </Button>
+        </div>
+      </DialogContent>
+    </Dialog>
+  );
+}

+ 11 - 1
src/app/v1/_lib/proxy/forwarder.ts

@@ -2891,7 +2891,17 @@ export class ProxyForwarder {
     // - 'disabled': 不应用(已在调度阶段被过滤)
     // - 'force_enable': 强制应用(仅对支持的模型)
     // - 'inherit' 或 null: 遵循客户端请求
-    if (session.getContext1mApplied?.()) {
+    if (
+      session.getContext1mApplied?.() &&
+      (provider.providerType === "claude" || provider.providerType === "claude-auth")
+    ) {
+      session.addSpecialSetting({
+        type: "anthropic_context_1m_header_override",
+        scope: "request_header",
+        hit: true,
+        header: "anthropic-beta",
+        flag: CONTEXT_1M_BETA_HEADER,
+      });
       const existingBeta =
         overrides["anthropic-beta"] || session.headers.get("anthropic-beta") || "";
       const betaFlags = new Set(

+ 246 - 115
src/app/v1/_lib/proxy/response-handler.ts

@@ -11,6 +11,7 @@ import { SessionTracker } from "@/lib/session-tracker";
 import type { CostBreakdown } from "@/lib/utils/cost-calculation";
 import { calculateRequestCost, calculateRequestCostBreakdown } from "@/lib/utils/cost-calculation";
 import { hasValidPriceData } from "@/lib/utils/price-data";
+import { resolvePricingForModelRecords } from "@/lib/utils/pricing-resolution";
 import { isSSEText, parseSSEData } from "@/lib/utils/sse";
 import {
   detectUpstreamErrorFromSseOrJsonText,
@@ -23,6 +24,7 @@ import {
 } from "@/repository/message";
 import { findLatestPriceByModel } from "@/repository/model-price";
 import { getSystemSettings } from "@/repository/system-config";
+import type { Provider } from "@/types/provider";
 import type { SessionUsageUpdate } from "@/types/session";
 import { GeminiAdapter } from "../gemini/adapter";
 import type { GeminiResponse } from "../gemini/types";
@@ -108,6 +110,123 @@ function cleanResponseHeaders(headers: Headers): Headers {
   return cleaned;
 }
 
+function ensurePricingResolutionSpecialSetting(
+  session: ProxySession,
+  resolvedPricing: Awaited<ReturnType<ProxySession["getResolvedPricingByBillingSource"]>>
+): void {
+  if (!resolvedPricing) return;
+
+  const existing = session
+    .getSpecialSettings()
+    ?.find(
+      (setting) =>
+        setting.type === "pricing_resolution" &&
+        setting.resolvedModelName === resolvedPricing.resolvedModelName &&
+        setting.resolvedPricingProviderKey === resolvedPricing.resolvedPricingProviderKey &&
+        setting.source === resolvedPricing.source
+    );
+
+  if (existing) return;
+
+  session.addSpecialSetting({
+    type: "pricing_resolution",
+    scope: "billing",
+    hit: true,
+    modelName: session.getCurrentModel() ?? resolvedPricing.resolvedModelName,
+    resolvedModelName: resolvedPricing.resolvedModelName,
+    resolvedPricingProviderKey: resolvedPricing.resolvedPricingProviderKey,
+    source: resolvedPricing.source,
+  });
+}
+
+function getRequestedCodexServiceTier(session: ProxySession): string | null {
+  if (session.provider?.providerType !== "codex") {
+    return null;
+  }
+
+  const request = session.request.message as Record<string, unknown>;
+  return typeof request.service_tier === "string" ? request.service_tier : null;
+}
+
+export function parseServiceTierFromResponseText(responseText: string): string | null {
+  let lastSeenServiceTier: string | null = null;
+
+  const applyValue = (value: unknown) => {
+    if (typeof value === "string" && value.trim()) {
+      lastSeenServiceTier = value.trim();
+    }
+  };
+
+  try {
+    const parsedValue = JSON.parse(responseText);
+    if (parsedValue && typeof parsedValue === "object" && !Array.isArray(parsedValue)) {
+      const parsed = parsedValue as Record<string, unknown>;
+      applyValue(parsed.service_tier);
+      if (parsed.response && typeof parsed.response === "object") {
+        applyValue((parsed.response as Record<string, unknown>).service_tier);
+      }
+    }
+  } catch {
+    // ignore, fallback to SSE parsing below
+  }
+
+  if (lastSeenServiceTier) {
+    return lastSeenServiceTier;
+  }
+
+  if (isSSEText(responseText)) {
+    const events = parseSSEData(responseText);
+    for (const event of events) {
+      if (!event.data || typeof event.data !== "object") continue;
+      const data = event.data as Record<string, unknown>;
+      applyValue(data.service_tier);
+      if (data.response && typeof data.response === "object") {
+        applyValue((data.response as Record<string, unknown>).service_tier);
+      }
+    }
+  }
+
+  return lastSeenServiceTier;
+}
+
+function isPriorityServiceTierApplied(
+  session: ProxySession,
+  actualServiceTier: string | null
+): boolean {
+  if (actualServiceTier != null) {
+    return actualServiceTier === "priority";
+  }
+  return getRequestedCodexServiceTier(session) === "priority";
+}
+
+function ensureCodexServiceTierResultSpecialSetting(
+  session: ProxySession,
+  actualServiceTier: string | null
+): void {
+  if (session.provider?.providerType !== "codex") {
+    return;
+  }
+
+  const requestedServiceTier = getRequestedCodexServiceTier(session);
+  const effectivePriority = isPriorityServiceTierApplied(session, actualServiceTier);
+  const existing = session
+    .getSpecialSettings()
+    ?.find((setting) => setting.type === "codex_service_tier_result");
+
+  if (existing && existing.type === "codex_service_tier_result") {
+    return;
+  }
+
+  session.addSpecialSetting({
+    type: "codex_service_tier_result",
+    scope: "response",
+    hit: effectivePriority || requestedServiceTier != null || actualServiceTier != null,
+    requestedServiceTier,
+    actualServiceTier,
+    effectivePriority,
+  });
+}
+
 type FinalizeDeferredStreamingResult = {
   /**
    * “内部结算用”的状态码。
@@ -386,7 +505,7 @@ async function finalizeDeferredStreamingFinalizationIfNeeded(
       const { recordEndpointSuccess } = await import("@/lib/endpoint-circuit-breaker");
       await recordEndpointSuccess(meta.endpointId);
     } catch (endpointError) {
-      logger.warn("[ResponseHandler] Failed to record endpoint success (stream)", {
+      logger.warn("[ResponseHandler] Failed to record endpoint success (stream finalized)", {
         endpointId: meta.endpointId,
         providerId: meta.providerId,
         error: endpointError,
@@ -441,7 +560,7 @@ async function finalizeDeferredStreamingFinalizationIfNeeded(
       providerId: meta.providerId,
       providerName: meta.providerName,
     }).catch((err) => {
-      logger.error("[ResponseHandler] Failed to update session provider info (stream)", {
+      logger.error("[ResponseHandler] Failed to update session provider info (stream finalized)", {
         error: err,
       });
     });
@@ -730,6 +849,9 @@ export class ProxyResponseHandler {
         const usageResult = parseUsageFromResponseText(responseText, provider.providerType);
         usageRecord = usageResult.usageRecord;
         usageMetrics = usageResult.usageMetrics;
+        const actualServiceTier = parseServiceTierFromResponseText(responseText);
+        ensureCodexServiceTierResultSpecialSetting(session, actualServiceTier);
+        const priorityServiceTierApplied = isPriorityServiceTierApplied(session, actualServiceTier);
 
         if (usageMetrics) {
           usageMetrics = normalizeUsageWithSwap(
@@ -775,12 +897,14 @@ export class ProxyResponseHandler {
             session.getOriginalModel(),
             session.getCurrentModel(),
             usageMetrics,
+            provider,
             provider.costMultiplier,
-            session.getContext1mApplied()
+            session.getContext1mApplied(),
+            priorityServiceTierApplied
           );
 
           // 追踪消费到 Redis(用于限流)
-          await trackCostToRedis(session, usageMetrics);
+          await trackCostToRedis(session, usageMetrics, priorityServiceTierApplied);
         }
 
         // Calculate cost for session tracking (with multiplier) and Langfuse (raw)
@@ -790,13 +914,15 @@ export class ProxyResponseHandler {
         if (usageMetrics) {
           try {
             if (session.request.model) {
-              const priceData = await session.getCachedPriceDataByBillingSource();
-              if (priceData) {
+              const resolvedPricing = await session.getResolvedPricingByBillingSource(provider);
+              if (resolvedPricing) {
+                ensurePricingResolutionSpecialSetting(session, resolvedPricing);
                 const cost = calculateRequestCost(
                   usageMetrics,
-                  priceData,
+                  resolvedPricing.priceData,
                   provider.costMultiplier,
-                  session.getContext1mApplied()
+                  session.getContext1mApplied(),
+                  priorityServiceTierApplied
                 );
                 if (cost.gt(0)) {
                   costUsdStr = cost.toString();
@@ -805,9 +931,10 @@ export class ProxyResponseHandler {
                 if (provider.costMultiplier !== 1) {
                   const rawCost = calculateRequestCost(
                     usageMetrics,
-                    priceData,
+                    resolvedPricing.priceData,
                     1.0,
-                    session.getContext1mApplied()
+                    session.getContext1mApplied(),
+                    priorityServiceTierApplied
                   );
                   if (rawCost.gt(0)) {
                     rawCostUsdStr = rawCost.toString();
@@ -819,8 +946,9 @@ export class ProxyResponseHandler {
                 try {
                   costBreakdown = calculateRequestCostBreakdown(
                     usageMetrics,
-                    priceData,
-                    session.getContext1mApplied()
+                    resolvedPricing.priceData,
+                    session.getContext1mApplied(),
+                    priorityServiceTierApplied
                   );
                 } catch {
                   /* non-critical */
@@ -895,7 +1023,8 @@ export class ProxyResponseHandler {
             model: session.getCurrentModel() ?? undefined, // 更新重定向后的模型
             providerId: session.provider?.id, // 更新最终供应商ID(重试切换后)
             context1mApplied: session.getContext1mApplied(),
-            swapCacheTtlApplied: provider.swapCacheTtlBilling ?? false,
+            swapCacheTtlApplied: session.provider?.swapCacheTtlBilling ?? false,
+            specialSettings: session.getSpecialSettings() ?? undefined,
           });
 
           // 记录请求结束
@@ -1179,17 +1308,23 @@ export class ProxyResponseHandler {
             };
 
             // 优先填充 head;超过 head 上限后切到 tail(但不代表一定发生截断,只有 tail 溢出才算截断)
-            if (!inTailMode) {
-              if (headBufferedBytes + bytes <= MAX_STATS_HEAD_BYTES) {
+            if (!inTailMode && headBufferedBytes < MAX_STATS_HEAD_BYTES) {
+              const remainingHeadBytes = MAX_STATS_HEAD_BYTES - headBufferedBytes;
+              if (remainingHeadBytes > 0 && bytes > remainingHeadBytes) {
+                const headPart = text.substring(0, remainingHeadBytes);
+                const tailPart = text.substring(remainingHeadBytes);
+
+                pushChunk(headPart, remainingHeadBytes);
+
+                inTailMode = true;
+                pushChunk(tailPart, bytes - remainingHeadBytes);
+              } else {
                 headChunks.push(text);
                 headBufferedBytes += bytes;
-                return;
               }
-
-              inTailMode = true;
+            } else {
+              pushChunk(text, bytes);
             }
-
-            pushToTail();
           };
           const decoder = new TextDecoder();
           let isFirstChunk = true;
@@ -1304,6 +1439,7 @@ export class ProxyResponseHandler {
                     const headText = decoder.decode(headPart, { stream: true });
                     pushChunk(headText, remainingHeadBytes);
 
+                    inTailMode = true;
                     const tailText = decoder.decode(tailPart, { stream: true });
                     pushChunk(tailText, chunkSize - remainingHeadBytes);
                   } else {
@@ -1486,6 +1622,7 @@ export class ProxyResponseHandler {
               });
             }
             try {
+              // 取消 reader lock
               reader?.releaseLock();
             } catch (e) {
               logger.warn("[ResponseHandler] Gemini passthrough: Failed to release reader lock", {
@@ -1574,7 +1711,7 @@ export class ProxyResponseHandler {
     const statusCode = response.status;
 
     // 使用 AsyncTaskManager 管理后台处理任务
-    const taskId = `stream-${messageContext.id}`;
+    const taskId = `stream-${messageContext?.id || `unknown-${Date.now()}`}`;
     const abortController = new AbortController();
 
     // ⭐ 提升 idleTimeoutId 到外部作用域,以便客户端断开时能清除
@@ -1688,7 +1825,7 @@ export class ProxyResponseHandler {
             allContent,
             session.requestSequence
           ).catch((err) => {
-            logger.error("[ResponseHandler] Failed to store stream response:", err);
+            logger.error("[ResponseHandler] Failed to store response:", err);
           });
         }
 
@@ -1701,6 +1838,10 @@ export class ProxyResponseHandler {
         const usageResult = parseUsageFromResponseText(allContent, provider.providerType);
         usageForCost = usageResult.usageMetrics;
 
+        const actualServiceTier = parseServiceTierFromResponseText(allContent);
+        ensureCodexServiceTierResultSpecialSetting(session, actualServiceTier);
+        const priorityServiceTierApplied = isPriorityServiceTierApplied(session, actualServiceTier);
+
         if (usageForCost) {
           usageForCost = normalizeUsageWithSwap(
             usageForCost,
@@ -1740,12 +1881,14 @@ export class ProxyResponseHandler {
           session.getOriginalModel(),
           session.getCurrentModel(),
           usageForCost,
+          provider,
           provider.costMultiplier,
-          session.getContext1mApplied()
+          session.getContext1mApplied(),
+          priorityServiceTierApplied
         );
 
         // 追踪消费到 Redis(用于限流)
-        await trackCostToRedis(session, usageForCost);
+        await trackCostToRedis(session, usageForCost, priorityServiceTierApplied);
 
         // Calculate cost for session tracking (with multiplier) and Langfuse (raw)
         let costUsdStr: string | undefined;
@@ -1754,13 +1897,15 @@ export class ProxyResponseHandler {
         if (usageForCost) {
           try {
             if (session.request.model) {
-              const priceData = await session.getCachedPriceDataByBillingSource();
-              if (priceData) {
+              const resolvedPricing = await session.getResolvedPricingByBillingSource(provider);
+              if (resolvedPricing) {
+                ensurePricingResolutionSpecialSetting(session, resolvedPricing);
                 const cost = calculateRequestCost(
                   usageForCost,
-                  priceData,
+                  resolvedPricing.priceData,
                   provider.costMultiplier,
-                  session.getContext1mApplied()
+                  session.getContext1mApplied(),
+                  priorityServiceTierApplied
                 );
                 if (cost.gt(0)) {
                   costUsdStr = cost.toString();
@@ -1769,9 +1914,10 @@ export class ProxyResponseHandler {
                 if (provider.costMultiplier !== 1) {
                   const rawCost = calculateRequestCost(
                     usageForCost,
-                    priceData,
+                    resolvedPricing.priceData,
                     1.0,
-                    session.getContext1mApplied()
+                    session.getContext1mApplied(),
+                    priorityServiceTierApplied
                   );
                   if (rawCost.gt(0)) {
                     rawCostUsdStr = rawCost.toString();
@@ -1783,8 +1929,9 @@ export class ProxyResponseHandler {
                 try {
                   costBreakdown = calculateRequestCostBreakdown(
                     usageForCost,
-                    priceData,
-                    session.getContext1mApplied()
+                    resolvedPricing.priceData,
+                    session.getContext1mApplied(),
+                    priorityServiceTierApplied
                   );
                 } catch {
                   /* non-critical */
@@ -1838,6 +1985,7 @@ export class ProxyResponseHandler {
           providerId: providerIdForPersistence ?? session.provider?.id, // 更新最终供应商ID(重试切换后)
           context1mApplied: session.getContext1mApplied(),
           swapCacheTtlApplied: provider.swapCacheTtlBilling ?? false,
+          specialSettings: session.getSpecialSettings() ?? undefined,
         });
 
         emitLangfuseTrace(session, {
@@ -1862,6 +2010,7 @@ export class ProxyResponseHandler {
             logger.info("ResponseHandler: Stream processing cancelled", {
               taskId,
               providerId: provider.id,
+              providerName: provider.name,
               chunksCollected: chunks.length,
             });
             break; // 提前终止
@@ -2172,7 +2321,7 @@ export class ProxyResponseHandler {
   }
 }
 
-function extractUsageMetrics(value: unknown): UsageMetrics | null {
+export function extractUsageMetrics(value: unknown): UsageMetrics | null {
   if (!value || typeof value !== "object") {
     return null;
   }
@@ -2202,7 +2351,7 @@ function extractUsageMetrics(value: unknown): UsageMetrics | null {
   }
 
   // OpenAI chat completion format: prompt_tokens → input_tokens
-  // Priority: Claude (input_tokens) > Gemini (promptTokenCount) > OpenAI (prompt_tokens)
+  // Priority: Claude (input_tokens) > Gemini (candidatesTokenCount) > OpenAI (prompt_tokens)
   if (result.input_tokens === undefined && typeof usage.prompt_tokens === "number") {
     result.input_tokens = usage.prompt_tokens;
     hasAny = true;
@@ -2386,7 +2535,7 @@ function extractUsageMetrics(value: unknown): UsageMetrics | null {
     if (inputTokensDetails && typeof inputTokensDetails.cached_tokens === "number") {
       result.cache_read_input_tokens = inputTokensDetails.cached_tokens;
       hasAny = true;
-      logger.debug("[UsageMetrics] Extracted cached tokens from OpenAI Response API format", {
+      logger.debug("[ResponseHandler] Parsed cached tokens from OpenAI Response API format", {
         cachedTokens: inputTokensDetails.cached_tokens,
       });
     }
@@ -2695,7 +2844,7 @@ function normalizeUsageWithSwap(
 
   let resolvedCacheTtl = swapped.cache_ttl ?? session.getCacheTtlResolved?.() ?? null;
 
-  // When the upstream response had no cache_ttl, we fell through to the session-level
+  // When the upstream response had no cache_ttlwe fell through to the session-level
   // getCacheTtlResolved() fallback which reflects the *original* (un-swapped) value.
   // We must invert it here to stay consistent with the already-swapped bucket tokens.
   if (swapCacheTtlBilling && !usageMetrics.cache_ttl) {
@@ -2726,8 +2875,10 @@ async function updateRequestCostFromUsage(
   originalModel: string | null,
   redirectedModel: string | null,
   usage: UsageMetrics | null,
+  provider: Provider | null,
   costMultiplier: number = 1.0,
-  context1mApplied: boolean = false
+  context1mApplied: boolean = false,
+  priorityServiceTierApplied: boolean = false
 ): Promise<void> {
   if (!usage) {
     logger.warn("[CostCalculation] No usage data, skipping cost update", {
@@ -2742,20 +2893,16 @@ async function updateRequestCostFromUsage(
   }
 
   try {
-    // 获取系统设置中的计费模型来源配置
     const systemSettings = await getSystemSettings();
     const billingModelSource = systemSettings.billingModelSource;
 
-    // 根据配置决定计费模型优先级
     let primaryModel: string | null;
     let fallbackModel: string | null;
 
     if (billingModelSource === "original") {
-      // 优先使用重定向前的原始模型
       primaryModel = originalModel;
       fallbackModel = redirectedModel;
     } else {
-      // 优先使用重定向后的实际模型
       primaryModel = redirectedModel;
       fallbackModel = originalModel;
     }
@@ -2767,50 +2914,21 @@ async function updateRequestCostFromUsage(
       fallbackModel,
     });
 
-    // Fallback 逻辑:优先主要模型,找不到则用备选模型
-    let priceData = null;
-    let usedModelForPricing = null;
-
-    const resolveValidPriceData = async (modelName: string) => {
-      const record = await findLatestPriceByModel(modelName);
-      const data = record?.priceData;
-      if (!data || !hasValidPriceData(data)) {
-        return null;
-      }
-      return record;
-    };
-
-    // Step 1: 尝试主要模型
-    if (primaryModel) {
-      const resolved = await resolveValidPriceData(primaryModel);
-      if (resolved) {
-        priceData = resolved;
-        usedModelForPricing = primaryModel;
-        logger.debug("[CostCalculation] Using primary model for pricing", {
-          messageId,
-          model: primaryModel,
-          billingModelSource,
-        });
-      }
-    }
-
-    // Step 2: Fallback 到备选模型
-    if (!priceData && fallbackModel && fallbackModel !== primaryModel) {
-      const resolved = await resolveValidPriceData(fallbackModel);
-      if (resolved) {
-        priceData = resolved;
-        usedModelForPricing = fallbackModel;
-        logger.warn("[CostCalculation] Primary model price not found, using fallback model", {
-          messageId,
-          primaryModel,
-          fallbackModel,
-          billingModelSource,
-        });
-      }
-    }
+    const primaryRecord = primaryModel ? await findLatestPriceByModel(primaryModel) : null;
+    const fallbackRecord =
+      fallbackModel && fallbackModel !== primaryModel
+        ? await findLatestPriceByModel(fallbackModel)
+        : null;
+
+    const resolvedPricing = resolvePricingForModelRecords({
+      provider,
+      primaryModelName: primaryModel,
+      fallbackModelName: fallbackModel,
+      primaryRecord,
+      fallbackRecord,
+    });
 
-    // Step 3: 完全失败(无价格或价格表暂不可用):不计费放行,并异步触发一次同步
-    if (!priceData?.priceData) {
+    if (!resolvedPricing?.priceData || !hasValidPriceData(resolvedPricing.priceData)) {
       logger.warn("[CostCalculation] No price data found, skipping billing", {
         messageId,
         originalModel,
@@ -2822,13 +2940,19 @@ async function updateRequestCostFromUsage(
       return;
     }
 
-    // 计算费用
-    const cost = calculateRequestCost(usage, priceData.priceData, costMultiplier, context1mApplied);
+    const cost = calculateRequestCost(
+      usage,
+      resolvedPricing.priceData,
+      costMultiplier,
+      context1mApplied,
+      priorityServiceTierApplied
+    );
 
     logger.info("[CostCalculation] Cost calculated successfully", {
       messageId,
-      usedModelForPricing,
-      billingModelSource,
+      usedModelForPricing: resolvedPricing.resolvedModelName,
+      resolvedPricingProviderKey: resolvedPricing.resolvedPricingProviderKey,
+      pricingResolutionSource: resolvedPricing.source,
       costUsd: cost.toString(),
       costMultiplier,
       usage,
@@ -2839,11 +2963,12 @@ async function updateRequestCostFromUsage(
     } else {
       logger.warn("[CostCalculation] Calculated cost is zero or negative", {
         messageId,
-        usedModelForPricing,
+        usedModelForPricing: resolvedPricing.resolvedModelName,
+        resolvedPricingProviderKey: resolvedPricing.resolvedPricingProviderKey,
         costUsd: cost.toString(),
         priceData: {
-          inputCost: priceData.priceData.input_cost_per_token,
-          outputCost: priceData.priceData.output_cost_per_token,
+          inputCost: resolvedPricing.priceData.input_cost_per_token,
+          outputCost: resolvedPricing.priceData.output_cost_per_token,
         },
       });
     }
@@ -2877,27 +3002,21 @@ export async function finalizeRequestStats(
   }
 
   const providerIdForPersistence = providerIdOverride ?? session.provider?.id;
-
-  // 1. 结束请求状态追踪
-  ProxyStatusTracker.getInstance().endRequest(messageContext.user.id, messageContext.id);
-
-  // 2. 更新请求时长
-  await updateMessageRequestDuration(messageContext.id, duration);
-
-  // 3. 解析 usage metrics
   const { usageMetrics } = parseUsageFromResponseText(responseText, provider.providerType);
-
+  const actualServiceTier = parseServiceTierFromResponseText(responseText);
+  ensureCodexServiceTierResultSpecialSetting(session, actualServiceTier);
+  const priorityServiceTierApplied = isPriorityServiceTierApplied(session, actualServiceTier);
   if (!usageMetrics) {
-    // 即使没有 usageMetrics,也需要更新状态码和 provider chain
     await updateMessageRequestDetails(messageContext.id, {
       statusCode: statusCode,
       ...(errorMessage ? { errorMessage } : {}),
       ttfbMs: session.ttfbMs ?? duration,
       providerChain: session.getProviderChain(),
       model: session.getCurrentModel() ?? undefined,
-      providerId: providerIdForPersistence, // 更新最终供应商ID(重试切换后)
+      providerId: providerIdForPersistence,
       context1mApplied: session.getContext1mApplied(),
-      swapCacheTtlApplied: provider.swapCacheTtlBilling ?? false,
+      swapCacheTtlApplied: session.provider?.swapCacheTtlBilling ?? false,
+      specialSettings: session.getSpecialSettings() ?? undefined,
     });
     return null;
   }
@@ -2916,25 +3035,29 @@ export async function finalizeRequestStats(
     session.getOriginalModel(),
     session.getCurrentModel(),
     normalizedUsage,
+    provider,
     provider.costMultiplier,
-    session.getContext1mApplied()
+    session.getContext1mApplied(),
+    priorityServiceTierApplied
   );
 
   // 5. 追踪消费到 Redis(用于限流)
-  await trackCostToRedis(session, normalizedUsage);
+  await trackCostToRedis(session, normalizedUsage, priorityServiceTierApplied);
 
   // 6. 更新 session usage
   if (session.sessionId) {
     let costUsdStr: string | undefined;
     try {
       if (session.request.model) {
-        const priceData = await session.getCachedPriceDataByBillingSource();
-        if (priceData) {
+        const resolvedPricing = await session.getResolvedPricingByBillingSource(provider);
+        if (resolvedPricing) {
+          ensurePricingResolutionSpecialSetting(session, resolvedPricing);
           const cost = calculateRequestCost(
             normalizedUsage,
-            priceData,
+            resolvedPricing.priceData,
             provider.costMultiplier,
-            session.getContext1mApplied()
+            session.getContext1mApplied(),
+            priorityServiceTierApplied
           );
           if (cost.gt(0)) {
             costUsdStr = cost.toString();
@@ -2978,6 +3101,7 @@ export async function finalizeRequestStats(
     providerId: providerIdForPersistence, // 更新最终供应商ID(重试切换后)
     context1mApplied: session.getContext1mApplied(),
     swapCacheTtlApplied: provider.swapCacheTtlBilling ?? false,
+    specialSettings: session.getSpecialSettings() ?? undefined,
   });
 
   return normalizedUsage;
@@ -2986,7 +3110,11 @@ export async function finalizeRequestStats(
 /**
  * 追踪消费到 Redis(用于限流)
  */
-async function trackCostToRedis(session: ProxySession, usage: UsageMetrics | null): Promise<void> {
+async function trackCostToRedis(
+  session: ProxySession,
+  usage: UsageMetrics | null,
+  priorityServiceTierApplied: boolean = false
+): Promise<void> {
   if (!usage || !session.sessionId) return;
 
   try {
@@ -3000,15 +3128,17 @@ async function trackCostToRedis(session: ProxySession, usage: UsageMetrics | nul
     const modelName = session.request.model;
     if (!modelName) return;
 
-    // 计算成本(应用倍率)- 使用 session 缓存避免重复查询
-    const priceData = await session.getCachedPriceDataByBillingSource();
-    if (!priceData) return;
+    const resolvedPricing = await session.getResolvedPricingByBillingSource(provider);
+    if (!resolvedPricing) return;
+
+    ensurePricingResolutionSpecialSetting(session, resolvedPricing);
 
     const cost = calculateRequestCost(
       usage,
-      priceData,
+      resolvedPricing.priceData,
       provider.costMultiplier,
-      session.getContext1mApplied()
+      session.getContext1mApplied(),
+      priorityServiceTierApplied
     );
     if (cost.lte(0)) return;
 
@@ -3137,6 +3267,7 @@ async function persistRequestFailure(options: {
       providerId: session.provider?.id, // 更新最终供应商ID(重试切换后)
       context1mApplied: session.getContext1mApplied(),
       swapCacheTtlApplied: session.provider?.swapCacheTtlBilling ?? false,
+      specialSettings: session.getSpecialSettings() ?? undefined,
     });
 
     const isAsyncWrite = getEnvConfig().MESSAGE_REQUEST_WRITE_MODE !== "sync";

+ 59 - 37
src/app/v1/_lib/proxy/session.ts

@@ -1,7 +1,10 @@
 import type { Context } from "hono";
 import { logger } from "@/lib/logger";
 import { clientRequestsContext1m as clientRequestsContext1mHelper } from "@/lib/special-attributes";
-import { hasValidPriceData } from "@/lib/utils/price-data";
+import {
+  type ResolvedPricing,
+  resolvePricingForModelRecords,
+} from "@/lib/utils/pricing-resolution";
 import { findLatestPriceByModel } from "@/repository/model-price";
 import { findAllProviders } from "@/repository/provider";
 import type { CacheTtlResolved } from "@/types/cache";
@@ -120,8 +123,8 @@ export class ProxySession {
    */
   private billingModelSourcePromise?: Promise<"original" | "redirected">;
 
-  // Cached price data for billing model source (lazy loaded: undefined=not loaded, null=no data)
-  private cachedBillingPriceData?: ModelPriceData | null;
+  // Resolved pricing cache (per request/provider combination)
+  private resolvedPricingCache = new Map<string, ResolvedPricing | null>();
 
   /**
    * 请求级 Provider 快照
@@ -701,30 +704,15 @@ export class ProxySession {
     return this.cachedPriceData ?? null;
   }
 
-  /**
-   * 根据系统配置的计费模型来源获取价格数据(带缓存)
-   *
-   * billingModelSource:
-   * - "original": 优先使用重定向前模型(getOriginalModel)
-   * - "redirected": 优先使用重定向后模型(request.model)
-   *
-   * Fallback:主模型无价格时尝试备选模型。
-   *
-   * @returns 价格数据;无模型或无价格时返回 null
-   */
-  async getCachedPriceDataByBillingSource(): Promise<ModelPriceData | null> {
-    if (this.cachedBillingPriceData !== undefined) {
-      return this.cachedBillingPriceData;
-    }
-
+  async getResolvedPricingByBillingSource(
+    provider?: Provider | null
+  ): Promise<ResolvedPricing | null> {
     const originalModel = this.getOriginalModel();
     const redirectedModel = this.request.model;
     if (!originalModel && !redirectedModel) {
-      this.cachedBillingPriceData = null;
       return null;
     }
 
-    // 懒加载配置(每请求只读取一次;并发安全)
     if (this.cachedBillingModelSource === undefined) {
       if (!this.billingModelSourcePromise) {
         this.billingModelSourcePromise = (async () => {
@@ -751,30 +739,64 @@ export class ProxySession {
       this.cachedBillingModelSource = await this.billingModelSourcePromise;
     }
 
+    const providerIdentity = provider ?? this.provider;
+    const cacheKey = [
+      this.cachedBillingModelSource,
+      originalModel ?? "",
+      redirectedModel ?? "",
+      providerIdentity?.id ?? 0,
+      providerIdentity?.name ?? "",
+      providerIdentity?.url ?? "",
+    ].join("|");
+
+    if (this.resolvedPricingCache.has(cacheKey)) {
+      return this.resolvedPricingCache.get(cacheKey) ?? null;
+    }
+
     const useOriginal = this.cachedBillingModelSource === "original";
     const primaryModel = useOriginal ? originalModel : redirectedModel;
     const fallbackModel = useOriginal ? redirectedModel : originalModel;
 
-    const findValidPriceDataByModel = async (modelName: string): Promise<ModelPriceData | null> => {
-      const result = await findLatestPriceByModel(modelName);
-      const data = result?.priceData;
-      if (!data || !hasValidPriceData(data)) {
-        return null;
-      }
-      return data;
-    };
+    const primaryRecord = primaryModel ? await findLatestPriceByModel(primaryModel) : null;
+    let resolved = resolvePricingForModelRecords({
+      provider: providerIdentity,
+      primaryModelName: primaryModel,
+      fallbackModelName: null,
+      primaryRecord,
+      fallbackRecord: null,
+    });
 
-    let priceData: ModelPriceData | null = null;
-    if (primaryModel) {
-      priceData = await findValidPriceDataByModel(primaryModel);
+    if (!resolved && fallbackModel && fallbackModel !== primaryModel) {
+      const fallbackRecord = await findLatestPriceByModel(fallbackModel);
+      resolved = resolvePricingForModelRecords({
+        provider: providerIdentity,
+        primaryModelName: primaryModel,
+        fallbackModelName: fallbackModel,
+        primaryRecord,
+        fallbackRecord,
+      });
     }
 
-    if (!priceData && fallbackModel && fallbackModel !== primaryModel) {
-      priceData = await findValidPriceDataByModel(fallbackModel);
-    }
+    this.resolvedPricingCache.set(cacheKey, resolved ?? null);
+    return resolved ?? null;
+  }
 
-    this.cachedBillingPriceData = priceData;
-    return this.cachedBillingPriceData;
+  /**
+   * 根据系统配置的计费模型来源获取价格数据(带缓存)
+   *
+   * billingModelSource:
+   * - "original": 优先使用重定向前模型(getOriginalModel)
+   * - "redirected": 优先使用重定向后模型(request.model)
+   *
+   * Fallback:主模型无价格时尝试备选模型。
+   *
+   * @returns 价格数据;无模型或无价格时返回 null
+   */
+  async getCachedPriceDataByBillingSource(
+    provider?: Provider | null
+  ): Promise<ModelPriceData | null> {
+    const resolved = await this.getResolvedPricingByBillingSource(provider);
+    return resolved?.priceData ?? null;
   }
 }
 

+ 223 - 168
src/lib/utils/cost-calculation.ts

@@ -6,6 +6,8 @@ import {
 import type { ModelPriceData } from "@/types/model-price";
 import { COST_SCALE, Decimal, toDecimal } from "./currency";
 
+const OPENAI_LONG_CONTEXT_TOKEN_THRESHOLD = 272000;
+
 type UsageMetrics = {
   input_tokens?: number;
   output_tokens?: number;
@@ -39,7 +41,7 @@ function multiplyCost(quantity: number | undefined, unitCost: number | undefined
  * @param threshold - 阈值(默认200k)
  * @returns 费用
  */
-function calculateTieredCost(
+function _calculateTieredCost(
   tokens: number,
   baseCostPerToken: number,
   premiumMultiplier: number,
@@ -55,13 +57,7 @@ function calculateTieredCost(
     return new Decimal(tokens).mul(baseCostDecimal);
   }
 
-  // 阈值内的token按基础费率计算
-  const baseCost = new Decimal(threshold).mul(baseCostDecimal);
-  // 超出阈值的token按溢价费率计算
-  const premiumTokens = tokens - threshold;
-  const premiumCost = new Decimal(premiumTokens).mul(baseCostDecimal).mul(premiumMultiplier);
-
-  return baseCost.add(premiumCost);
+  return new Decimal(tokens).mul(baseCostDecimal).mul(premiumMultiplier);
 }
 
 /**
@@ -72,7 +68,7 @@ function calculateTieredCost(
  * @param threshold - 阈值(默认 200K)
  * @returns 费用
  */
-function calculateTieredCostWithSeparatePrices(
+function __calculateTieredCostWithSeparatePrices(
   tokens: number,
   baseCostPerToken: number,
   premiumCostPerToken: number,
@@ -89,13 +85,41 @@ function calculateTieredCostWithSeparatePrices(
     return new Decimal(tokens).mul(baseCostDecimal);
   }
 
-  // 阈值内的 token 按基础费率计算
-  const baseCost = new Decimal(threshold).mul(baseCostDecimal);
-  // 超出阈值的 token 按溢价费率计算
-  const premiumTokens = tokens - threshold;
-  const premiumCost = new Decimal(premiumTokens).mul(premiumCostDecimal);
+  return new Decimal(tokens).mul(premiumCostDecimal);
+}
+
+function resolveLongContextThreshold(priceData: ModelPriceData): number {
+  const has272kFields =
+    typeof priceData.input_cost_per_token_above_272k_tokens === "number" ||
+    typeof priceData.output_cost_per_token_above_272k_tokens === "number" ||
+    typeof priceData.cache_creation_input_token_cost_above_272k_tokens === "number" ||
+    typeof priceData.cache_read_input_token_cost_above_272k_tokens === "number" ||
+    typeof priceData.cache_creation_input_token_cost_above_1hr_above_272k_tokens === "number";
+
+  const modelFamily = typeof priceData.model_family === "string" ? priceData.model_family : "";
+  if (has272kFields || modelFamily === "gpt" || modelFamily === "gpt-pro") {
+    return OPENAI_LONG_CONTEXT_TOKEN_THRESHOLD;
+  }
+
+  return CONTEXT_1M_TOKEN_THRESHOLD;
+}
 
-  return baseCost.add(premiumCost);
+function getRequestInputContextTokens(
+  usage: UsageMetrics,
+  cache5mTokens?: number,
+  cache1hTokens?: number
+): number {
+  const cacheCreationInputTokens =
+    typeof usage.cache_creation_input_tokens === "number"
+      ? usage.cache_creation_input_tokens
+      : (cache5mTokens ?? 0) + (cache1hTokens ?? 0);
+
+  return (
+    (usage.input_tokens ?? 0) +
+    cacheCreationInputTokens +
+    (usage.cache_read_input_tokens ?? 0) +
+    (usage.input_image_tokens ?? 0)
+  );
 }
 
 export interface CostBreakdown {
@@ -113,15 +137,24 @@ export interface CostBreakdown {
 export function calculateRequestCostBreakdown(
   usage: UsageMetrics,
   priceData: ModelPriceData,
-  context1mApplied: boolean = false
+  context1mApplied: boolean = false,
+  priorityServiceTierApplied: boolean = false
 ): CostBreakdown {
   let inputBucket = new Decimal(0);
   let outputBucket = new Decimal(0);
   let cacheCreationBucket = new Decimal(0);
   let cacheReadBucket = new Decimal(0);
 
-  const inputCostPerToken = priceData.input_cost_per_token;
-  const outputCostPerToken = priceData.output_cost_per_token;
+  const baseInputCostPerToken = priceData.input_cost_per_token;
+  const baseOutputCostPerToken = priceData.output_cost_per_token;
+  const inputCostPerToken =
+    priorityServiceTierApplied && typeof priceData.input_cost_per_token_priority === "number"
+      ? priceData.input_cost_per_token_priority
+      : baseInputCostPerToken;
+  const outputCostPerToken =
+    priorityServiceTierApplied && typeof priceData.output_cost_per_token_priority === "number"
+      ? priceData.output_cost_per_token_priority
+      : baseOutputCostPerToken;
   const inputCostPerRequest = priceData.input_cost_per_request;
 
   // Per-request cost -> input bucket
@@ -138,19 +171,22 @@ export function calculateRequestCostBreakdown(
 
   const cacheCreation5mCost =
     priceData.cache_creation_input_token_cost ??
-    (inputCostPerToken != null ? inputCostPerToken * 1.25 : undefined);
+    (baseInputCostPerToken != null ? baseInputCostPerToken * 1.25 : undefined);
 
   const cacheCreation1hCost =
     priceData.cache_creation_input_token_cost_above_1hr ??
-    (inputCostPerToken != null ? inputCostPerToken * 2 : undefined) ??
+    (baseInputCostPerToken != null ? baseInputCostPerToken * 2 : undefined) ??
     cacheCreation5mCost;
 
   const cacheReadCost =
-    priceData.cache_read_input_token_cost ??
-    (inputCostPerToken != null
-      ? inputCostPerToken * 0.1
-      : outputCostPerToken != null
-        ? outputCostPerToken * 0.1
+    (priorityServiceTierApplied &&
+    typeof priceData.cache_read_input_token_cost_priority === "number"
+      ? priceData.cache_read_input_token_cost_priority
+      : priceData.cache_read_input_token_cost) ??
+    (baseInputCostPerToken != null
+      ? baseInputCostPerToken * 0.1
+      : baseOutputCostPerToken != null
+        ? baseOutputCostPerToken * 0.1
         : undefined);
 
   // Derive cache creation tokens by TTL
@@ -171,92 +207,110 @@ export function calculateRequestCostBreakdown(
     }
   }
 
-  const inputAbove200k = priceData.input_cost_per_token_above_200k_tokens;
-  const outputAbove200k = priceData.output_cost_per_token_above_200k_tokens;
+  const inputAboveThreshold =
+    priceData.input_cost_per_token_above_272k_tokens ??
+    priceData.input_cost_per_token_above_200k_tokens;
+  const outputAboveThreshold =
+    priceData.output_cost_per_token_above_272k_tokens ??
+    priceData.output_cost_per_token_above_200k_tokens;
+  const cacheCreationAboveThreshold =
+    priceData.cache_creation_input_token_cost_above_272k_tokens ??
+    priceData.cache_creation_input_token_cost_above_200k_tokens;
+  const cacheCreation1hAboveThreshold =
+    priceData.cache_creation_input_token_cost_above_1hr_above_272k_tokens ??
+    priceData.cache_creation_input_token_cost_above_1hr_above_200k_tokens ??
+    cacheCreationAboveThreshold;
+  const cacheReadAboveThreshold =
+    priceData.cache_read_input_token_cost_above_272k_tokens ??
+    priceData.cache_read_input_token_cost_above_200k_tokens;
+  const longContextThreshold = resolveLongContextThreshold(priceData);
+  const longContextThresholdExceeded =
+    getRequestInputContextTokens(usage, cache5mTokens, cache1hTokens) > longContextThreshold;
+  const hasRealCacheCreationBase = priceData.cache_creation_input_token_cost != null;
+  const hasRealCacheReadBase = priceData.cache_read_input_token_cost != null;
 
   // Input tokens -> input bucket
-  if (context1mApplied && inputCostPerToken != null && usage.input_tokens != null) {
-    inputBucket = inputBucket.add(
-      calculateTieredCost(
-        usage.input_tokens,
-        inputCostPerToken,
-        CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER
-      )
-    );
-  } else if (inputAbove200k != null && inputCostPerToken != null && usage.input_tokens != null) {
+  // 注意:一旦请求的“输入上下文总量”超过阈值,供应商官方定价按整次请求的全量 token
+  // 应用 long-context 价格,而不是仅对超过阈值的部分加价。
+  if (longContextThresholdExceeded && inputAboveThreshold != null && usage.input_tokens != null) {
+    inputBucket = inputBucket.add(multiplyCost(usage.input_tokens, inputAboveThreshold));
+  } else if (
+    longContextThresholdExceeded &&
+    context1mApplied &&
+    !priorityServiceTierApplied &&
+    inputCostPerToken != null &&
+    usage.input_tokens != null
+  ) {
     inputBucket = inputBucket.add(
-      calculateTieredCostWithSeparatePrices(usage.input_tokens, inputCostPerToken, inputAbove200k)
+      multiplyCost(usage.input_tokens, inputCostPerToken * CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
     );
   } else {
     inputBucket = inputBucket.add(multiplyCost(usage.input_tokens, inputCostPerToken));
   }
 
   // Output tokens -> output bucket
-  if (context1mApplied && outputCostPerToken != null && usage.output_tokens != null) {
-    outputBucket = outputBucket.add(
-      calculateTieredCost(
-        usage.output_tokens,
-        outputCostPerToken,
-        CONTEXT_1M_OUTPUT_PREMIUM_MULTIPLIER
-      )
-    );
-  } else if (outputAbove200k != null && outputCostPerToken != null && usage.output_tokens != null) {
+  // 与 input 相同:阈值判断基于整次请求的输入上下文,而不是 output bucket 自己的 token 数。
+  if (longContextThresholdExceeded && outputAboveThreshold != null && usage.output_tokens != null) {
+    outputBucket = outputBucket.add(multiplyCost(usage.output_tokens, outputAboveThreshold));
+  } else if (
+    longContextThresholdExceeded &&
+    context1mApplied &&
+    !priorityServiceTierApplied &&
+    outputCostPerToken != null &&
+    usage.output_tokens != null
+  ) {
     outputBucket = outputBucket.add(
-      calculateTieredCostWithSeparatePrices(
-        usage.output_tokens,
-        outputCostPerToken,
-        outputAbove200k
-      )
+      multiplyCost(usage.output_tokens, outputCostPerToken * CONTEXT_1M_OUTPUT_PREMIUM_MULTIPLIER)
     );
   } else {
     outputBucket = outputBucket.add(multiplyCost(usage.output_tokens, outputCostPerToken));
   }
 
   // Cache costs
-  const cacheCreationAbove200k = priceData.cache_creation_input_token_cost_above_200k_tokens;
-  const cacheReadAbove200k = priceData.cache_read_input_token_cost_above_200k_tokens;
-  const hasRealCacheCreationBase = priceData.cache_creation_input_token_cost != null;
-  const hasRealCacheReadBase = priceData.cache_read_input_token_cost != null;
 
   // Cache creation 5m -> cache_creation bucket
-  if (context1mApplied && cacheCreation5mCost != null && cache5mTokens != null) {
+  if (
+    longContextThresholdExceeded &&
+    hasRealCacheCreationBase &&
+    cacheCreationAboveThreshold != null &&
+    cache5mTokens != null
+  ) {
     cacheCreationBucket = cacheCreationBucket.add(
-      calculateTieredCost(cache5mTokens, cacheCreation5mCost, CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
+      multiplyCost(cache5mTokens, cacheCreationAboveThreshold)
     );
   } else if (
-    hasRealCacheCreationBase &&
-    cacheCreationAbove200k != null &&
+    longContextThresholdExceeded &&
+    context1mApplied &&
+    !priorityServiceTierApplied &&
     cacheCreation5mCost != null &&
     cache5mTokens != null
   ) {
     cacheCreationBucket = cacheCreationBucket.add(
-      calculateTieredCostWithSeparatePrices(
-        cache5mTokens,
-        cacheCreation5mCost,
-        cacheCreationAbove200k
-      )
+      multiplyCost(cache5mTokens, cacheCreation5mCost * CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
     );
   } else {
     cacheCreationBucket = cacheCreationBucket.add(multiplyCost(cache5mTokens, cacheCreation5mCost));
   }
 
   // Cache creation 1h -> cache_creation bucket
-  if (context1mApplied && cacheCreation1hCost != null && cache1hTokens != null) {
+  if (
+    longContextThresholdExceeded &&
+    hasRealCacheCreationBase &&
+    cacheCreation1hAboveThreshold != null &&
+    cache1hTokens != null
+  ) {
     cacheCreationBucket = cacheCreationBucket.add(
-      calculateTieredCost(cache1hTokens, cacheCreation1hCost, CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
+      multiplyCost(cache1hTokens, cacheCreation1hAboveThreshold)
     );
   } else if (
-    hasRealCacheCreationBase &&
-    cacheCreationAbove200k != null &&
+    longContextThresholdExceeded &&
+    context1mApplied &&
+    !priorityServiceTierApplied &&
     cacheCreation1hCost != null &&
     cache1hTokens != null
   ) {
     cacheCreationBucket = cacheCreationBucket.add(
-      calculateTieredCostWithSeparatePrices(
-        cache1hTokens,
-        cacheCreation1hCost,
-        cacheCreationAbove200k
-      )
+      multiplyCost(cache1hTokens, cacheCreation1hCost * CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
     );
   } else {
     cacheCreationBucket = cacheCreationBucket.add(multiplyCost(cache1hTokens, cacheCreation1hCost));
@@ -264,17 +318,13 @@ export function calculateRequestCostBreakdown(
 
   // Cache read -> cache_read bucket
   if (
+    longContextThresholdExceeded &&
     hasRealCacheReadBase &&
-    cacheReadAbove200k != null &&
-    cacheReadCost != null &&
+    cacheReadAboveThreshold != null &&
     usage.cache_read_input_tokens != null
   ) {
     cacheReadBucket = cacheReadBucket.add(
-      calculateTieredCostWithSeparatePrices(
-        usage.cache_read_input_tokens,
-        cacheReadCost,
-        cacheReadAbove200k
-      )
+      multiplyCost(usage.cache_read_input_tokens, cacheReadAboveThreshold)
     );
   } else {
     cacheReadBucket = cacheReadBucket.add(
@@ -318,12 +368,21 @@ export function calculateRequestCost(
   usage: UsageMetrics,
   priceData: ModelPriceData,
   multiplier: number = 1.0,
-  context1mApplied: boolean = false
+  context1mApplied: boolean = false,
+  priorityServiceTierApplied: boolean = false
 ): Decimal {
   const segments: Decimal[] = [];
 
-  const inputCostPerToken = priceData.input_cost_per_token;
-  const outputCostPerToken = priceData.output_cost_per_token;
+  const baseInputCostPerToken = priceData.input_cost_per_token;
+  const baseOutputCostPerToken = priceData.output_cost_per_token;
+  const inputCostPerToken =
+    priorityServiceTierApplied && typeof priceData.input_cost_per_token_priority === "number"
+      ? priceData.input_cost_per_token_priority
+      : baseInputCostPerToken;
+  const outputCostPerToken =
+    priorityServiceTierApplied && typeof priceData.output_cost_per_token_priority === "number"
+      ? priceData.output_cost_per_token_priority
+      : baseOutputCostPerToken;
   const inputCostPerRequest = priceData.input_cost_per_request;
 
   if (
@@ -339,19 +398,22 @@ export function calculateRequestCost(
 
   const cacheCreation5mCost =
     priceData.cache_creation_input_token_cost ??
-    (inputCostPerToken != null ? inputCostPerToken * 1.25 : undefined);
+    (baseInputCostPerToken != null ? baseInputCostPerToken * 1.25 : undefined);
 
   const cacheCreation1hCost =
     priceData.cache_creation_input_token_cost_above_1hr ??
-    (inputCostPerToken != null ? inputCostPerToken * 2 : undefined) ??
+    (baseInputCostPerToken != null ? baseInputCostPerToken * 2 : undefined) ??
     cacheCreation5mCost;
 
   const cacheReadCost =
-    priceData.cache_read_input_token_cost ??
-    (inputCostPerToken != null
-      ? inputCostPerToken * 0.1
-      : outputCostPerToken != null
-        ? outputCostPerToken * 0.1
+    (priorityServiceTierApplied &&
+    typeof priceData.cache_read_input_token_cost_priority === "number"
+      ? priceData.cache_read_input_token_cost_priority
+      : priceData.cache_read_input_token_cost) ??
+    (baseInputCostPerToken != null
+      ? baseInputCostPerToken * 0.1
+      : baseOutputCostPerToken != null
+        ? baseOutputCostPerToken * 0.1
         : undefined);
 
   // Derive cache creation tokens by TTL
@@ -372,126 +434,119 @@ export function calculateRequestCost(
     }
   }
 
-  // 检查是否有 200K 分层价格(Gemini 等模型)
-  const inputAbove200k = priceData.input_cost_per_token_above_200k_tokens;
-  const outputAbove200k = priceData.output_cost_per_token_above_200k_tokens;
+  const inputAboveThreshold =
+    priceData.input_cost_per_token_above_272k_tokens ??
+    priceData.input_cost_per_token_above_200k_tokens;
+  const outputAboveThreshold =
+    priceData.output_cost_per_token_above_272k_tokens ??
+    priceData.output_cost_per_token_above_200k_tokens;
+  const cacheCreationAboveThreshold =
+    priceData.cache_creation_input_token_cost_above_272k_tokens ??
+    priceData.cache_creation_input_token_cost_above_200k_tokens;
+  const cacheCreation1hAboveThreshold =
+    priceData.cache_creation_input_token_cost_above_1hr_above_272k_tokens ??
+    priceData.cache_creation_input_token_cost_above_1hr_above_200k_tokens ??
+    cacheCreationAboveThreshold;
+  const cacheReadAboveThreshold =
+    priceData.cache_read_input_token_cost_above_272k_tokens ??
+    priceData.cache_read_input_token_cost_above_200k_tokens;
+  const longContextThreshold = resolveLongContextThreshold(priceData);
+  const longContextThresholdExceeded =
+    getRequestInputContextTokens(usage, cache5mTokens, cache1hTokens) > longContextThreshold;
+  const hasRealCacheCreationBase = priceData.cache_creation_input_token_cost != null;
+  const hasRealCacheReadBase = priceData.cache_read_input_token_cost != null;
 
-  // 计算 input 费用:优先级 context1mApplied > 200K分层 > 普通
-  if (context1mApplied && inputCostPerToken != null && usage.input_tokens != null) {
-    // Claude 1M context: 使用倍数计算
-    segments.push(
-      calculateTieredCost(
-        usage.input_tokens,
-        inputCostPerToken,
-        CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER
-      )
-    );
-  } else if (inputAbove200k != null && inputCostPerToken != null && usage.input_tokens != null) {
-    // Gemini 等: 使用独立价格字段
+  // Input tokens
+  // 注意:阈值命中后按整次请求的全量 token 应用 long-context 价格。
+  if (longContextThresholdExceeded && inputAboveThreshold != null && usage.input_tokens != null) {
+    segments.push(multiplyCost(usage.input_tokens, inputAboveThreshold));
+  } else if (
+    longContextThresholdExceeded &&
+    context1mApplied &&
+    !priorityServiceTierApplied &&
+    inputCostPerToken != null &&
+    usage.input_tokens != null
+  ) {
     segments.push(
-      calculateTieredCostWithSeparatePrices(usage.input_tokens, inputCostPerToken, inputAbove200k)
+      multiplyCost(usage.input_tokens, inputCostPerToken * CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
     );
   } else {
-    // 普通计算
     segments.push(multiplyCost(usage.input_tokens, inputCostPerToken));
   }
 
-  // 计算 output 费用:优先级 context1mApplied > 200K分层 > 普通
-  if (context1mApplied && outputCostPerToken != null && usage.output_tokens != null) {
-    // Claude 1M context: 使用倍数计算
-    segments.push(
-      calculateTieredCost(
-        usage.output_tokens,
-        outputCostPerToken,
-        CONTEXT_1M_OUTPUT_PREMIUM_MULTIPLIER
-      )
-    );
-  } else if (outputAbove200k != null && outputCostPerToken != null && usage.output_tokens != null) {
-    // Gemini 等: 使用独立价格字段
+  // Output tokens
+  if (longContextThresholdExceeded && outputAboveThreshold != null && usage.output_tokens != null) {
+    segments.push(multiplyCost(usage.output_tokens, outputAboveThreshold));
+  } else if (
+    longContextThresholdExceeded &&
+    context1mApplied &&
+    !priorityServiceTierApplied &&
+    outputCostPerToken != null &&
+    usage.output_tokens != null
+  ) {
     segments.push(
-      calculateTieredCostWithSeparatePrices(
-        usage.output_tokens,
-        outputCostPerToken,
-        outputAbove200k
-      )
+      multiplyCost(usage.output_tokens, outputCostPerToken * CONTEXT_1M_OUTPUT_PREMIUM_MULTIPLIER)
     );
   } else {
-    // 普通计算
     segments.push(multiplyCost(usage.output_tokens, outputCostPerToken));
   }
 
   // 缓存相关费用
   // 检查是否有 200K 分层的缓存价格
   // 注意:只有当价格表中的原始基础价格存在时才启用分层计费,避免派生价格与分层价格混用导致误计费
-  const cacheCreationAbove200k = priceData.cache_creation_input_token_cost_above_200k_tokens;
-  const cacheReadAbove200k = priceData.cache_read_input_token_cost_above_200k_tokens;
-  const hasRealCacheCreationBase = priceData.cache_creation_input_token_cost != null;
-  const hasRealCacheReadBase = priceData.cache_read_input_token_cost != null;
 
-  // 缓存创建费用(5分钟 TTL):优先级 context1mApplied > 200K分层 > 普通
-  if (context1mApplied && cacheCreation5mCost != null && cache5mTokens != null) {
-    // Claude 1M context: 使用 input 倍数计算(cache creation 属于 input 类别)
-    segments.push(
-      calculateTieredCost(cache5mTokens, cacheCreation5mCost, CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
-    );
-  } else if (
+  // 缓存创建费用(5分钟 TTL):优先级 explicit long-context > context1m fallback > 普通
+  if (
+    longContextThresholdExceeded &&
     hasRealCacheCreationBase &&
-    cacheCreationAbove200k != null &&
+    cacheCreationAboveThreshold != null &&
+    cache5mTokens != null
+  ) {
+    segments.push(multiplyCost(cache5mTokens, cacheCreationAboveThreshold));
+  } else if (
+    longContextThresholdExceeded &&
+    context1mApplied &&
+    !priorityServiceTierApplied &&
     cacheCreation5mCost != null &&
     cache5mTokens != null
   ) {
-    // Gemini 等: 使用独立价格字段
     segments.push(
-      calculateTieredCostWithSeparatePrices(
-        cache5mTokens,
-        cacheCreation5mCost,
-        cacheCreationAbove200k
-      )
+      multiplyCost(cache5mTokens, cacheCreation5mCost * CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
     );
   } else {
-    // 普通计算
     segments.push(multiplyCost(cache5mTokens, cacheCreation5mCost));
   }
 
-  // 缓存创建费用(1小时 TTL):优先级 context1mApplied > 200K分层 > 普通
-  if (context1mApplied && cacheCreation1hCost != null && cache1hTokens != null) {
-    // Claude 1M context: 使用 input 倍数计算(cache creation 属于 input 类别)
-    segments.push(
-      calculateTieredCost(cache1hTokens, cacheCreation1hCost, CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
-    );
-  } else if (
+  // 缓存创建费用(1小时 TTL):优先级 explicit long-context > context1m fallback > 普通
+  if (
+    longContextThresholdExceeded &&
     hasRealCacheCreationBase &&
-    cacheCreationAbove200k != null &&
+    cacheCreation1hAboveThreshold != null &&
+    cache1hTokens != null
+  ) {
+    segments.push(multiplyCost(cache1hTokens, cacheCreation1hAboveThreshold));
+  } else if (
+    longContextThresholdExceeded &&
+    context1mApplied &&
+    !priorityServiceTierApplied &&
     cacheCreation1hCost != null &&
     cache1hTokens != null
   ) {
-    // Gemini 等: 使用独立价格字段
     segments.push(
-      calculateTieredCostWithSeparatePrices(
-        cache1hTokens,
-        cacheCreation1hCost,
-        cacheCreationAbove200k
-      )
+      multiplyCost(cache1hTokens, cacheCreation1hCost * CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
     );
   } else {
-    // 普通计算
     segments.push(multiplyCost(cache1hTokens, cacheCreation1hCost));
   }
 
   // 缓存读取费用
   if (
+    longContextThresholdExceeded &&
     hasRealCacheReadBase &&
-    cacheReadAbove200k != null &&
-    cacheReadCost != null &&
+    cacheReadAboveThreshold != null &&
     usage.cache_read_input_tokens != null
   ) {
-    segments.push(
-      calculateTieredCostWithSeparatePrices(
-        usage.cache_read_input_tokens,
-        cacheReadCost,
-        cacheReadAbove200k
-      )
-    );
+    segments.push(multiplyCost(usage.cache_read_input_tokens, cacheReadAboveThreshold));
   } else {
     segments.push(multiplyCost(usage.cache_read_input_tokens, cacheReadCost));
   }

+ 34 - 12
src/lib/utils/price-data.ts

@@ -1,11 +1,11 @@
 import type { ModelPriceData } from "@/types/model-price";
 
-/**
- * 判断价格数据是否包含至少一个可用于计费的价格字段。
- * 避免把数据库中的 `{}` 或仅包含元信息的记录当成有效价格。
- */
-export function hasValidPriceData(priceData: ModelPriceData): boolean {
-  const numericCosts = [
+function hasValidNumericPrice(values: unknown[]): boolean {
+  return values.some((value) => typeof value === "number" && Number.isFinite(value) && value >= 0);
+}
+
+function collectNumericCosts(priceData: ModelPriceData): unknown[] {
+  return [
     priceData.input_cost_per_token,
     priceData.output_cost_per_token,
     priceData.input_cost_per_request,
@@ -16,15 +16,39 @@ export function hasValidPriceData(priceData: ModelPriceData): boolean {
     priceData.output_cost_per_token_above_200k_tokens,
     priceData.cache_creation_input_token_cost_above_200k_tokens,
     priceData.cache_read_input_token_cost_above_200k_tokens,
+    priceData.cache_creation_input_token_cost_above_1hr_above_200k_tokens,
+    priceData.input_cost_per_token_above_272k_tokens,
+    priceData.output_cost_per_token_above_272k_tokens,
+    priceData.cache_creation_input_token_cost_above_272k_tokens,
+    priceData.cache_read_input_token_cost_above_272k_tokens,
+    priceData.cache_creation_input_token_cost_above_1hr_above_272k_tokens,
+    priceData.input_cost_per_token_priority,
+    priceData.output_cost_per_token_priority,
+    priceData.cache_read_input_token_cost_priority,
     priceData.output_cost_per_image,
   ];
+}
 
-  if (
-    numericCosts.some((value) => typeof value === "number" && Number.isFinite(value) && value >= 0)
-  ) {
+/**
+ * 判断价格数据是否包含至少一个可用于计费的价格字段。
+ * 避免把数据库中的 `{}` 或仅包含元信息的记录当成有效价格。
+ */
+export function hasValidPriceData(priceData: ModelPriceData): boolean {
+  if (hasValidNumericPrice(collectNumericCosts(priceData))) {
     return true;
   }
 
+  const pricing = priceData.pricing;
+  if (pricing && typeof pricing === "object" && !Array.isArray(pricing)) {
+    for (const value of Object.values(pricing)) {
+      if (value && typeof value === "object" && !Array.isArray(value)) {
+        if (hasValidNumericPrice(Object.values(value))) {
+          return true;
+        }
+      }
+    }
+  }
+
   const searchCosts = priceData.search_context_cost_per_query;
   if (searchCosts) {
     const searchCostFields = [
@@ -32,9 +56,7 @@ export function hasValidPriceData(priceData: ModelPriceData): boolean {
       searchCosts.search_context_size_low,
       searchCosts.search_context_size_medium,
     ];
-    return searchCostFields.some(
-      (value) => typeof value === "number" && Number.isFinite(value) && value >= 0
-    );
+    return hasValidNumericPrice(searchCostFields);
   }
 
   return false;

+ 405 - 0
src/lib/utils/pricing-resolution.ts

@@ -0,0 +1,405 @@
+import type { ModelPrice, ModelPriceData } from "@/types/model-price";
+import type { Provider } from "@/types/provider";
+import { hasValidPriceData } from "./price-data";
+
+export type ResolvedPricingSource =
+  | "local_manual"
+  | "cloud_exact"
+  | "cloud_model_fallback"
+  | "priority_fallback"
+  | "single_provider_top_level"
+  | "official_fallback";
+
+export interface ResolvedPricing {
+  resolvedModelName: string;
+  resolvedPricingProviderKey: string;
+  source: ResolvedPricingSource;
+  priceData: ModelPriceData;
+  pricingNode?: Record<string, unknown> | null;
+}
+
+interface ModelRecordCandidate {
+  modelName: string | null;
+  record: ModelPrice | null;
+  isPrimary: boolean;
+}
+
+interface PricingKeyCandidate {
+  key: string;
+  type: "exact" | "official";
+}
+
+export interface ResolvePricingForModelRecordsInput {
+  provider: Provider | null | undefined;
+  primaryModelName: string | null;
+  fallbackModelName: string | null;
+  primaryRecord: ModelPrice | null;
+  fallbackRecord: ModelPrice | null;
+}
+
+const DETAIL_FIELDS = [
+  "input_cost_per_token",
+  "output_cost_per_token",
+  "input_cost_per_request",
+  "cache_creation_input_token_cost",
+  "cache_creation_input_token_cost_above_1hr",
+  "cache_read_input_token_cost",
+  "input_cost_per_token_above_200k_tokens",
+  "output_cost_per_token_above_200k_tokens",
+  "cache_creation_input_token_cost_above_200k_tokens",
+  "cache_read_input_token_cost_above_200k_tokens",
+  "cache_creation_input_token_cost_above_1hr_above_200k_tokens",
+  "input_cost_per_token_above_272k_tokens",
+  "output_cost_per_token_above_272k_tokens",
+  "cache_creation_input_token_cost_above_272k_tokens",
+  "cache_read_input_token_cost_above_272k_tokens",
+  "cache_creation_input_token_cost_above_1hr_above_272k_tokens",
+  "input_cost_per_token_priority",
+  "output_cost_per_token_priority",
+  "cache_read_input_token_cost_priority",
+  "output_cost_per_image",
+  "input_cost_per_image",
+] as const;
+
+const DETAIL_TIE_BREAK_ORDER = [
+  "openrouter",
+  "opencode",
+  "cloudflare-ai-gateway",
+  "github-copilot",
+  "chatgpt",
+] as const;
+
+function pushUnique(
+  candidates: PricingKeyCandidate[],
+  key: string,
+  type: PricingKeyCandidate["type"]
+) {
+  if (!key || candidates.some((candidate) => candidate.key === key)) {
+    return;
+  }
+  candidates.push({ key, type });
+}
+
+function normalizeText(value: string | null | undefined): string {
+  return (value ?? "").trim().toLowerCase();
+}
+
+function extractHost(urlValue: string | null | undefined): string {
+  if (!urlValue) return "";
+  try {
+    return new URL(urlValue).host.toLowerCase();
+  } catch {
+    return "";
+  }
+}
+
+function getOfficialProviderKeys(
+  modelName: string | null | undefined,
+  priceData?: ModelPriceData
+): string[] {
+  const family = normalizeText(
+    typeof priceData?.model_family === "string" ? priceData.model_family : ""
+  );
+  const normalizedModelName = normalizeText(modelName);
+
+  if (
+    family === "gpt" ||
+    family === "gpt-pro" ||
+    normalizedModelName.startsWith("gpt-") ||
+    normalizedModelName.includes("chatgpt")
+  ) {
+    return ["openai"];
+  }
+
+  if (family.startsWith("claude") || normalizedModelName.startsWith("claude")) {
+    return ["anthropic"];
+  }
+
+  if (family.includes("gemini") || normalizedModelName.startsWith("gemini")) {
+    return ["vertex_ai", "vertex", "google"];
+  }
+
+  return [];
+}
+
+export function resolvePricingKeyCandidates(
+  provider: Provider | null | undefined,
+  modelName: string | null | undefined,
+  priceData?: ModelPriceData
+): PricingKeyCandidate[] {
+  const candidates: PricingKeyCandidate[] = [];
+  const name = normalizeText(provider?.name);
+  const url = normalizeText(provider?.url);
+  const host = extractHost(provider?.url);
+
+  if (name.includes("openrouter") || host.includes("openrouter")) {
+    pushUnique(candidates, "openrouter", "exact");
+  }
+  if (name.includes("opencode") || host.includes("opencode")) {
+    pushUnique(candidates, "opencode", "exact");
+  }
+  if (
+    name.includes("cloudflare") ||
+    host.includes("cloudflare") ||
+    url.includes("cloudflare-ai-gateway")
+  ) {
+    pushUnique(candidates, "cloudflare-ai-gateway", "exact");
+  }
+  if (name.includes("github") || name.includes("copilot") || host.includes("githubcopilot")) {
+    pushUnique(candidates, "github-copilot", "exact");
+  }
+  if (name.includes("chatgpt") || host.includes("chatgpt.com")) {
+    pushUnique(candidates, "chatgpt", "exact");
+  }
+  if (name.includes("openai") || host.includes("openai.com") || host.includes("api.openai.com")) {
+    pushUnique(candidates, "openai", "exact");
+  }
+  if (name.includes("anthropic") || host.includes("anthropic.com")) {
+    pushUnique(candidates, "anthropic", "exact");
+  }
+  if (name.includes("vertex") || host.includes("googleapis.com") || name.includes("google")) {
+    pushUnique(candidates, "vertex_ai", "exact");
+    pushUnique(candidates, "vertex", "exact");
+    pushUnique(candidates, "google", "exact");
+  }
+
+  for (const officialKey of getOfficialProviderKeys(modelName, priceData)) {
+    pushUnique(candidates, officialKey, "official");
+  }
+
+  return candidates;
+}
+
+function getPricingMap(record: ModelPrice | null): Record<string, Record<string, unknown>> | null {
+  const pricing = record?.priceData?.pricing;
+  if (!pricing || typeof pricing !== "object" || Array.isArray(pricing)) {
+    return null;
+  }
+  return pricing;
+}
+
+function mergePriceData(
+  base: ModelPriceData,
+  pricingNode: Record<string, unknown> | null,
+  pricingProviderKey: string
+): ModelPriceData {
+  if (!pricingNode) {
+    return typeof base.selected_pricing_provider === "string"
+      ? {
+          ...base,
+          selected_pricing_provider: base.selected_pricing_provider,
+        }
+      : { ...base };
+  }
+
+  return {
+    ...base,
+    ...pricingNode,
+    pricing: base.pricing,
+    selected_pricing_provider: pricingProviderKey,
+  };
+}
+
+function getDetailScore(pricingNode: Record<string, unknown>): number {
+  return DETAIL_FIELDS.reduce((score, field) => {
+    const value = pricingNode[field];
+    return typeof value === "number" && Number.isFinite(value) ? score + 1 : score;
+  }, 0);
+}
+
+function compareDetailKeys(
+  a: string,
+  b: string,
+  pricingMap: Record<string, Record<string, unknown>>
+): number {
+  const scoreDiff = getDetailScore(pricingMap[b] ?? {}) - getDetailScore(pricingMap[a] ?? {});
+  if (scoreDiff !== 0) return scoreDiff;
+
+  const indexA = DETAIL_TIE_BREAK_ORDER.indexOf(a as (typeof DETAIL_TIE_BREAK_ORDER)[number]);
+  const indexB = DETAIL_TIE_BREAK_ORDER.indexOf(b as (typeof DETAIL_TIE_BREAK_ORDER)[number]);
+
+  if (indexA >= 0 || indexB >= 0) {
+    if (indexA < 0) return 1;
+    if (indexB < 0) return -1;
+    return indexA - indexB;
+  }
+
+  return a.localeCompare(b);
+}
+
+function resolveManualPricing(
+  record: ModelPrice,
+  modelName: string | null
+): ResolvedPricing | null {
+  if (!hasValidPriceData(record.priceData)) {
+    return null;
+  }
+
+  const resolvedPricingProviderKey =
+    (typeof record.priceData.selected_pricing_provider === "string" &&
+      record.priceData.selected_pricing_provider.trim()) ||
+    (typeof record.priceData.litellm_provider === "string" &&
+      record.priceData.litellm_provider.trim()) ||
+    "manual";
+
+  return {
+    resolvedModelName: modelName ?? record.modelName,
+    resolvedPricingProviderKey,
+    source: "local_manual",
+    priceData: mergePriceData(record.priceData, null, resolvedPricingProviderKey),
+    pricingNode: null,
+  };
+}
+
+function resolveFromPricingMap(
+  candidate: ModelRecordCandidate,
+  keyCandidates: PricingKeyCandidate[],
+  type: PricingKeyCandidate["type"]
+): ResolvedPricing | null {
+  const pricingMap = getPricingMap(candidate.record);
+  if (!candidate.record || !pricingMap) {
+    return null;
+  }
+
+  for (const keyCandidate of keyCandidates) {
+    if (keyCandidate.type !== type) continue;
+    const pricingNode = pricingMap[keyCandidate.key];
+    if (!pricingNode) continue;
+
+    const mergedPriceData = mergePriceData(
+      candidate.record.priceData,
+      pricingNode,
+      keyCandidate.key
+    );
+    if (!hasValidPriceData(mergedPriceData)) {
+      continue;
+    }
+
+    const source: ResolvedPricingSource =
+      type === "official"
+        ? "official_fallback"
+        : candidate.isPrimary
+          ? "cloud_exact"
+          : "cloud_model_fallback";
+
+    return {
+      resolvedModelName: candidate.modelName ?? candidate.record.modelName,
+      resolvedPricingProviderKey: keyCandidate.key,
+      source,
+      priceData: mergedPriceData,
+      pricingNode,
+    };
+  }
+
+  return null;
+}
+
+function resolveDetailedFallback(candidate: ModelRecordCandidate): ResolvedPricing | null {
+  const pricingMap = getPricingMap(candidate.record);
+  if (!candidate.record || !pricingMap) {
+    return null;
+  }
+
+  const keys = Object.keys(pricingMap).sort((a, b) => compareDetailKeys(a, b, pricingMap));
+  const selectedKey = keys[0];
+  if (!selectedKey) {
+    return null;
+  }
+
+  const pricingNode = pricingMap[selectedKey];
+  const mergedPriceData = mergePriceData(candidate.record.priceData, pricingNode, selectedKey);
+  if (!hasValidPriceData(mergedPriceData)) {
+    return null;
+  }
+
+  return {
+    resolvedModelName: candidate.modelName ?? candidate.record.modelName,
+    resolvedPricingProviderKey: selectedKey,
+    source: "priority_fallback",
+    priceData: mergedPriceData,
+    pricingNode,
+  };
+}
+
+function resolveTopLevel(candidate: ModelRecordCandidate): ResolvedPricing | null {
+  if (!candidate.record || !hasValidPriceData(candidate.record.priceData)) {
+    return null;
+  }
+
+  const officialKeys = getOfficialProviderKeys(candidate.modelName, candidate.record.priceData);
+  const resolvedPricingProviderKey =
+    (typeof candidate.record.priceData.selected_pricing_provider === "string" &&
+      candidate.record.priceData.selected_pricing_provider.trim()) ||
+    (typeof candidate.record.priceData.litellm_provider === "string" &&
+      candidate.record.priceData.litellm_provider.trim()) ||
+    officialKeys[0] ||
+    candidate.record.modelName;
+
+  return {
+    resolvedModelName: candidate.modelName ?? candidate.record.modelName,
+    resolvedPricingProviderKey,
+    source:
+      candidate.record.source === "manual"
+        ? "local_manual"
+        : candidate.isPrimary
+          ? "single_provider_top_level"
+          : "cloud_model_fallback",
+    priceData: mergePriceData(candidate.record.priceData, null, resolvedPricingProviderKey),
+    pricingNode: null,
+  };
+}
+
+export function resolvePricingForModelRecords(
+  input: ResolvePricingForModelRecordsInput
+): ResolvedPricing | null {
+  const candidates: ModelRecordCandidate[] = [
+    {
+      modelName: input.primaryModelName,
+      record: input.primaryRecord,
+      isPrimary: true,
+    },
+  ];
+
+  if (input.fallbackModelName && input.fallbackModelName !== input.primaryModelName) {
+    candidates.push({
+      modelName: input.fallbackModelName,
+      record: input.fallbackRecord,
+      isPrimary: false,
+    });
+  }
+
+  for (const candidate of candidates) {
+    if (candidate.record?.source === "manual") {
+      const resolved = resolveManualPricing(candidate.record, candidate.modelName);
+      if (resolved) return resolved;
+    }
+  }
+
+  const keyCandidates = resolvePricingKeyCandidates(
+    input.provider,
+    input.primaryModelName ?? input.fallbackModelName,
+    input.primaryRecord?.priceData ?? input.fallbackRecord?.priceData
+  );
+
+  for (const candidate of candidates) {
+    const resolved = resolveFromPricingMap(candidate, keyCandidates, "exact");
+    if (resolved) return resolved;
+  }
+
+  for (const candidate of candidates) {
+    const resolved = resolveFromPricingMap(candidate, keyCandidates, "official");
+    if (resolved) return resolved;
+  }
+
+  for (const candidate of candidates) {
+    const resolved = resolveDetailedFallback(candidate);
+    if (resolved) return resolved;
+  }
+
+  for (const candidate of candidates) {
+    const resolved = resolveTopLevel(candidate);
+    if (resolved) return resolved;
+  }
+
+  return null;
+}

+ 69 - 4
src/lib/utils/special-settings.ts

@@ -1,4 +1,3 @@
-import { CONTEXT_1M_BETA_HEADER } from "@/lib/special-attributes";
 import type { SpecialSetting } from "@/types/special-settings";
 
 type BuildUnifiedSpecialSettingsParams = {
@@ -23,7 +22,7 @@ type BuildUnifiedSpecialSettingsParams = {
    */
   cacheTtlApplied?: string | null;
   /**
-   * 1M 上下文是否应用(用于展示 1M 标头覆写命中
+   * 1M 上下文是否应用(保留参数用于兼容调用方;不再自动派生 header 覆写审计
    */
   context1mApplied?: boolean | null;
 };
@@ -108,6 +107,23 @@ function buildSettingKey(setting: SpecialSetting): string {
         setting.preference,
         setting.hadGoogleSearchInRequest,
       ]);
+    case "pricing_resolution":
+      return JSON.stringify([
+        setting.type,
+        setting.hit,
+        setting.modelName,
+        setting.resolvedModelName,
+        setting.resolvedPricingProviderKey,
+        setting.source,
+      ]);
+    case "codex_service_tier_result":
+      return JSON.stringify([
+        setting.type,
+        setting.hit,
+        setting.requestedServiceTier,
+        setting.actualServiceTier,
+        setting.effectivePriority,
+      ]);
     default: {
       // 兜底:保证即使未来扩展类型也不会导致运行时崩溃
       const _exhaustive: never = setting;
@@ -152,13 +168,21 @@ export function buildUnifiedSpecialSettings(
     });
   }
 
-  if (params.context1mApplied === true) {
+  const hasExplicitAnthropicContext1m = base.some(
+    (item) => item.type === "anthropic_context_1m_header_override"
+  );
+  const hasCodexServiceTierResult = base.some((item) => item.type === "codex_service_tier_result");
+  if (
+    params.context1mApplied === true &&
+    !hasExplicitAnthropicContext1m &&
+    !hasCodexServiceTierResult
+  ) {
     derived.push({
       type: "anthropic_context_1m_header_override",
       scope: "request_header",
       hit: true,
       header: "anthropic-beta",
-      flag: CONTEXT_1M_BETA_HEADER,
+      flag: "context-1m-2025-08-07",
     });
   }
 
@@ -185,6 +209,17 @@ export function hasPriorityServiceTierSpecialSetting(
     return false;
   }
 
+  const codexServiceTierResult = specialSettings.find(
+    (setting): setting is Extract<SpecialSetting, { type: "codex_service_tier_result" }> =>
+      setting.type === "codex_service_tier_result"
+  );
+  if (codexServiceTierResult) {
+    if (codexServiceTierResult.actualServiceTier != null) {
+      return codexServiceTierResult.actualServiceTier === "priority";
+    }
+    return codexServiceTierResult.effectivePriority;
+  }
+
   return specialSettings.some(
     (setting) =>
       setting.type === "provider_parameter_override" &&
@@ -194,3 +229,33 @@ export function hasPriorityServiceTierSpecialSetting(
       )
   );
 }
+
+export function getPriorityServiceTierSpecialSetting(
+  specialSettings?: SpecialSetting[] | null
+): Extract<SpecialSetting, { type: "codex_service_tier_result" }> | null {
+  if (!Array.isArray(specialSettings) || specialSettings.length === 0) {
+    return null;
+  }
+
+  return (
+    specialSettings.find(
+      (setting): setting is Extract<SpecialSetting, { type: "codex_service_tier_result" }> =>
+        setting.type === "codex_service_tier_result"
+    ) ?? null
+  );
+}
+
+export function getPricingResolutionSpecialSetting(
+  specialSettings?: SpecialSetting[] | null
+): Extract<SpecialSetting, { type: "pricing_resolution" }> | null {
+  if (!Array.isArray(specialSettings) || specialSettings.length === 0) {
+    return null;
+  }
+
+  return (
+    specialSettings.find(
+      (setting): setting is Extract<SpecialSetting, { type: "pricing_resolution" }> =>
+        setting.type === "pricing_resolution"
+    ) ?? null
+  );
+}

+ 64 - 2
src/repository/model-price.ts

@@ -1,6 +1,6 @@
 "use server";
 
-import { desc, eq, sql } from "drizzle-orm";
+import { desc, eq, inArray, sql } from "drizzle-orm";
 import { db } from "@/drizzle/db";
 import { modelPrices } from "@/drizzle/schema";
 import { logger } from "@/lib/logger";
@@ -48,7 +48,6 @@ export async function findLatestPriceByModel(modelName: string): Promise<ModelPr
       .from(modelPrices)
       .where(eq(modelPrices.modelName, modelName))
       .orderBy(
-        // 本地手动配置优先(哪怕云端数据更新得更晚)
         sql`(${modelPrices.source} = 'manual') DESC`,
         sql`${modelPrices.createdAt} DESC NULLS LAST`,
         desc(modelPrices.id)
@@ -66,6 +65,69 @@ export async function findLatestPriceByModel(modelName: string): Promise<ModelPr
   }
 }
 
+export async function findLatestPriceByModelAndSource(
+  modelName: string,
+  source: ModelPriceSource
+): Promise<ModelPrice | null> {
+  try {
+    const selection = {
+      id: modelPrices.id,
+      modelName: modelPrices.modelName,
+      priceData: modelPrices.priceData,
+      source: modelPrices.source,
+      createdAt: modelPrices.createdAt,
+      updatedAt: modelPrices.updatedAt,
+    };
+
+    const [price] = await db
+      .select(selection)
+      .from(modelPrices)
+      .where(sql`${modelPrices.modelName} = ${modelName} AND ${modelPrices.source} = ${source}`)
+      .orderBy(sql`${modelPrices.createdAt} DESC NULLS LAST`, desc(modelPrices.id))
+      .limit(1);
+
+    if (!price) return null;
+    return toModelPrice(price);
+  } catch (error) {
+    logger.error("[ModelPrice] Failed to query latest price by model and source", {
+      modelName,
+      source,
+      error: error instanceof Error ? error.message : String(error),
+    });
+    return null;
+  }
+}
+
+export async function findLatestPricesByModels(
+  modelNames: string[]
+): Promise<Map<string, ModelPrice>> {
+  const uniqueNames = Array.from(new Set(modelNames.map((name) => name.trim()).filter(Boolean)));
+  if (uniqueNames.length === 0) {
+    return new Map();
+  }
+
+  const query = sql`
+    SELECT DISTINCT ON (model_name)
+      id,
+      model_name as "modelName",
+      price_data as "priceData",
+      source,
+      created_at as "createdAt",
+      updated_at as "updatedAt"
+    FROM model_prices
+    WHERE ${inArray(modelPrices.modelName, uniqueNames)}
+    ORDER BY
+      model_name,
+      (source = 'manual') DESC,
+      created_at DESC NULLS LAST,
+      id DESC
+  `;
+
+  const result = await db.execute(query);
+  const rows = Array.from(result).map(toModelPrice);
+  return new Map(rows.map((row) => [row.modelName, row]));
+}
+
 /**
  * 获取所有模型的最新价格(非分页版本,保持向后兼容)
  * 注意:使用原生 SQL(DISTINCT ON),并确保 manual 来源优先

+ 18 - 1
src/types/model-price.ts

@@ -17,6 +17,19 @@ export interface ModelPriceData {
   output_cost_per_token_above_200k_tokens?: number;
   cache_creation_input_token_cost_above_200k_tokens?: number;
   cache_read_input_token_cost_above_200k_tokens?: number;
+  cache_creation_input_token_cost_above_1hr_above_200k_tokens?: number;
+
+  // 272K 分层价格(GPT-5.4 等模型保留扩展)
+  input_cost_per_token_above_272k_tokens?: number;
+  output_cost_per_token_above_272k_tokens?: number;
+  cache_creation_input_token_cost_above_272k_tokens?: number;
+  cache_read_input_token_cost_above_272k_tokens?: number;
+  cache_creation_input_token_cost_above_1hr_above_272k_tokens?: number;
+
+  // 优先服务等级价格(例如 OpenAI priority tier)
+  input_cost_per_token_priority?: number;
+  output_cost_per_token_priority?: number;
+  cache_read_input_token_cost_priority?: number;
 
   // 图片生成价格
   output_cost_per_image?: number;
@@ -38,10 +51,14 @@ export interface ModelPriceData {
   display_name?: string;
   litellm_provider?: string;
   providers?: string[];
+  pricing?: Record<string, Record<string, unknown>>;
+  selected_pricing_provider?: string;
+  selected_pricing_source_model?: string;
+  selected_pricing_resolution?: "manual_pin";
   max_input_tokens?: number;
   max_output_tokens?: number;
   max_tokens?: number;
-  mode?: "chat" | "image_generation" | "completion";
+  mode?: "chat" | "image_generation" | "completion" | "responses";
 
   // 支持的功能
   supports_assistant_prefill?: boolean;

+ 28 - 1
src/types/special-settings.ts

@@ -16,7 +16,9 @@ export type SpecialSetting =
   | ClaudeMetadataUserIdInjectionSpecialSetting
   | AnthropicCacheTtlHeaderOverrideSpecialSetting
   | AnthropicContext1mHeaderOverrideSpecialSetting
-  | GeminiGoogleSearchOverrideSpecialSetting;
+  | GeminiGoogleSearchOverrideSpecialSetting
+  | PricingResolutionSpecialSetting
+  | CodexServiceTierResultSpecialSetting;
 
 export type SpecialSettingChangeValue = string | number | boolean | null;
 
@@ -198,3 +200,28 @@ export type GeminiGoogleSearchOverrideSpecialSetting = {
   preference: "enabled" | "disabled";
   hadGoogleSearchInRequest: boolean;
 };
+
+export type PricingResolutionSpecialSetting = {
+  type: "pricing_resolution";
+  scope: "billing";
+  hit: boolean;
+  modelName: string;
+  resolvedModelName: string;
+  resolvedPricingProviderKey: string;
+  source:
+    | "local_manual"
+    | "cloud_exact"
+    | "cloud_model_fallback"
+    | "priority_fallback"
+    | "single_provider_top_level"
+    | "official_fallback";
+};
+
+export type CodexServiceTierResultSpecialSetting = {
+  type: "codex_service_tier_result";
+  scope: "response";
+  hit: boolean;
+  requestedServiceTier: string | null;
+  actualServiceTier: string | null;
+  effectivePriority: boolean;
+};

+ 251 - 2
tests/integration/billing-model-source.test.ts

@@ -141,11 +141,15 @@ function createSession({
   redirectedModel,
   sessionId,
   messageId,
+  providerOverrides,
+  requestMessage,
 }: {
   originalModel: string;
   redirectedModel: string;
   sessionId: string;
   messageId: number;
+  providerOverrides?: Record<string, unknown>;
+  requestMessage?: Record<string, unknown>;
 }): ProxySession {
   const session = new (
     ProxySession as unknown as {
@@ -167,7 +171,7 @@ function createSession({
     requestUrl: new URL("http://localhost/v1/messages"),
     headers: new Headers(),
     headerLog: "",
-    request: { message: {}, log: "(test)", model: redirectedModel },
+    request: { message: requestMessage ?? {}, log: "(test)", model: redirectedModel },
     userAgent: null,
     context: {},
     clientAbortSignal: null,
@@ -179,9 +183,11 @@ function createSession({
   const provider = {
     id: 99,
     name: "test-provider",
+    url: "https://api.anthropic.com",
     providerType: "claude",
     costMultiplier: 1.0,
     streamingIdleTimeoutMs: 0,
+    ...providerOverrides,
   } as any;
 
   const user = {
@@ -216,11 +222,15 @@ function createSession({
   return session;
 }
 
-function createNonStreamResponse(usage: { input_tokens: number; output_tokens: number }): Response {
+function createNonStreamResponse(
+  usage: { input_tokens: number; output_tokens: number },
+  extras?: Record<string, unknown>
+): Response {
   return new Response(
     JSON.stringify({
       type: "message",
       usage,
+      ...(extras ?? {}),
     }),
     {
       status: 200,
@@ -369,6 +379,245 @@ describe("Billing model source - Redis session cost vs DB cost", () => {
     expect(redirected.sessionCostUsd).toBe("50");
     expect(original.sessionCostUsd).not.toBe(redirected.sessionCostUsd);
   });
+
+  it("nested pricing: gpt-5.4 alias model should bill from pricing.openai when provider is chatgpt", async () => {
+    vi.mocked(getSystemSettings).mockResolvedValue(makeSystemSettings("redirected"));
+    vi.mocked(updateMessageRequestDetails).mockResolvedValue(undefined);
+    vi.mocked(updateMessageRequestDuration).mockResolvedValue(undefined);
+    vi.mocked(SessionManager.storeSessionResponse).mockResolvedValue(undefined);
+    vi.mocked(RateLimitService.trackUserDailyCost).mockResolvedValue(undefined);
+    vi.mocked(SessionTracker.refreshSession).mockResolvedValue(undefined);
+
+    vi.mocked(findLatestPriceByModel).mockImplementation(async (modelName: string) => {
+      if (modelName === "gpt-5.4") {
+        return makePriceRecord(modelName, {
+          mode: "responses",
+          model_family: "gpt",
+          litellm_provider: "chatgpt",
+          pricing: {
+            openai: {
+              input_cost_per_token: 2.5,
+              output_cost_per_token: 15,
+            },
+          },
+        });
+      }
+      return null;
+    });
+
+    const dbCosts: string[] = [];
+    vi.mocked(updateMessageRequestCost).mockImplementation(
+      async (_id: number, costUsd: unknown) => {
+        dbCosts.push(String(costUsd));
+      }
+    );
+
+    const sessionCosts: string[] = [];
+    vi.mocked(SessionManager.updateSessionUsage).mockImplementation(
+      async (_sessionId: string, payload: Record<string, unknown>) => {
+        if (typeof payload.costUsd === "string") {
+          sessionCosts.push(payload.costUsd);
+        }
+      }
+    );
+
+    const session = createSession({
+      originalModel: "gpt-5.4",
+      redirectedModel: "gpt-5.4",
+      sessionId: "sess-gpt54-chatgpt",
+      messageId: 3100,
+      providerOverrides: {
+        name: "ChatGPT",
+        url: "https://chatgpt.com/backend-api/codex",
+        providerType: "codex",
+      },
+    });
+
+    const response = createNonStreamResponse({ input_tokens: 2, output_tokens: 3 });
+    await ProxyResponseHandler.dispatch(session, response);
+    await drainAsyncTasks();
+
+    expect(dbCosts[0]).toBe("50");
+    expect(sessionCosts[0]).toBe("50");
+  });
+
+  it("codex fast: uses priority pricing when response reports service_tier=priority", async () => {
+    vi.mocked(getSystemSettings).mockResolvedValue(makeSystemSettings("redirected"));
+    vi.mocked(updateMessageRequestDetails).mockResolvedValue(undefined);
+    vi.mocked(updateMessageRequestDuration).mockResolvedValue(undefined);
+    vi.mocked(SessionManager.storeSessionResponse).mockResolvedValue(undefined);
+    vi.mocked(RateLimitService.trackUserDailyCost).mockResolvedValue(undefined);
+    vi.mocked(SessionTracker.refreshSession).mockResolvedValue(undefined);
+
+    vi.mocked(findLatestPriceByModel).mockImplementation(async (modelName: string) => {
+      if (modelName === "gpt-5.4") {
+        return makePriceRecord(modelName, {
+          mode: "responses",
+          model_family: "gpt",
+          litellm_provider: "chatgpt",
+          pricing: {
+            openai: {
+              input_cost_per_token: 1,
+              output_cost_per_token: 10,
+              input_cost_per_token_priority: 2,
+              output_cost_per_token_priority: 20,
+            },
+          },
+        });
+      }
+      return null;
+    });
+
+    const dbCosts: string[] = [];
+    vi.mocked(updateMessageRequestCost).mockImplementation(
+      async (_id: number, costUsd: unknown) => {
+        dbCosts.push(String(costUsd));
+      }
+    );
+
+    const sessionCosts: string[] = [];
+    vi.mocked(SessionManager.updateSessionUsage).mockImplementation(
+      async (_sessionId: string, payload: Record<string, unknown>) => {
+        if (typeof payload.costUsd === "string") {
+          sessionCosts.push(payload.costUsd);
+        }
+      }
+    );
+
+    const session = createSession({
+      originalModel: "gpt-5.4",
+      redirectedModel: "gpt-5.4",
+      sessionId: "sess-gpt54-priority-actual",
+      messageId: 3200,
+      providerOverrides: {
+        name: "ChatGPT",
+        url: "https://chatgpt.com/backend-api/codex",
+        providerType: "codex",
+      },
+      requestMessage: { service_tier: "default" },
+    });
+
+    const response = createNonStreamResponse(
+      { input_tokens: 2, output_tokens: 3 },
+      { service_tier: "priority" }
+    );
+    await ProxyResponseHandler.dispatch(session, response);
+    await drainAsyncTasks();
+
+    expect(dbCosts[0]).toBe("64");
+    expect(sessionCosts[0]).toBe("64");
+  });
+
+  it("codex fast: falls back to requested priority pricing when response omits service_tier", async () => {
+    vi.mocked(getSystemSettings).mockResolvedValue(makeSystemSettings("redirected"));
+    vi.mocked(updateMessageRequestDetails).mockResolvedValue(undefined);
+    vi.mocked(updateMessageRequestDuration).mockResolvedValue(undefined);
+    vi.mocked(SessionManager.storeSessionResponse).mockResolvedValue(undefined);
+    vi.mocked(RateLimitService.trackUserDailyCost).mockResolvedValue(undefined);
+    vi.mocked(SessionTracker.refreshSession).mockResolvedValue(undefined);
+
+    vi.mocked(findLatestPriceByModel).mockImplementation(async (modelName: string) => {
+      if (modelName === "gpt-5.4") {
+        return makePriceRecord(modelName, {
+          mode: "responses",
+          model_family: "gpt",
+          litellm_provider: "chatgpt",
+          pricing: {
+            openai: {
+              input_cost_per_token: 1,
+              output_cost_per_token: 10,
+              input_cost_per_token_priority: 2,
+              output_cost_per_token_priority: 20,
+            },
+          },
+        });
+      }
+      return null;
+    });
+
+    const dbCosts: string[] = [];
+    vi.mocked(updateMessageRequestCost).mockImplementation(
+      async (_id: number, costUsd: unknown) => {
+        dbCosts.push(String(costUsd));
+      }
+    );
+
+    const session = createSession({
+      originalModel: "gpt-5.4",
+      redirectedModel: "gpt-5.4",
+      sessionId: "sess-gpt54-priority-requested",
+      messageId: 3201,
+      providerOverrides: {
+        name: "ChatGPT",
+        url: "https://chatgpt.com/backend-api/codex",
+        providerType: "codex",
+      },
+      requestMessage: { service_tier: "priority" },
+    });
+
+    const response = createNonStreamResponse({ input_tokens: 2, output_tokens: 3 });
+    await ProxyResponseHandler.dispatch(session, response);
+    await drainAsyncTasks();
+
+    expect(dbCosts[0]).toBe("64");
+  });
+
+  it("codex fast: does not use priority pricing when response explicitly reports non-priority tier", async () => {
+    vi.mocked(getSystemSettings).mockResolvedValue(makeSystemSettings("redirected"));
+    vi.mocked(updateMessageRequestDetails).mockResolvedValue(undefined);
+    vi.mocked(updateMessageRequestDuration).mockResolvedValue(undefined);
+    vi.mocked(SessionManager.storeSessionResponse).mockResolvedValue(undefined);
+    vi.mocked(RateLimitService.trackUserDailyCost).mockResolvedValue(undefined);
+    vi.mocked(SessionTracker.refreshSession).mockResolvedValue(undefined);
+
+    vi.mocked(findLatestPriceByModel).mockImplementation(async (modelName: string) => {
+      if (modelName === "gpt-5.4") {
+        return makePriceRecord(modelName, {
+          mode: "responses",
+          model_family: "gpt",
+          litellm_provider: "chatgpt",
+          pricing: {
+            openai: {
+              input_cost_per_token: 1,
+              output_cost_per_token: 10,
+              input_cost_per_token_priority: 2,
+              output_cost_per_token_priority: 20,
+            },
+          },
+        });
+      }
+      return null;
+    });
+
+    const dbCosts: string[] = [];
+    vi.mocked(updateMessageRequestCost).mockImplementation(
+      async (_id: number, costUsd: unknown) => {
+        dbCosts.push(String(costUsd));
+      }
+    );
+
+    const session = createSession({
+      originalModel: "gpt-5.4",
+      redirectedModel: "gpt-5.4",
+      sessionId: "sess-gpt54-priority-downgraded",
+      messageId: 3202,
+      providerOverrides: {
+        name: "ChatGPT",
+        url: "https://chatgpt.com/backend-api/codex",
+        providerType: "codex",
+      },
+      requestMessage: { service_tier: "priority" },
+    });
+
+    const response = createNonStreamResponse(
+      { input_tokens: 2, output_tokens: 3 },
+      { service_tier: "default" }
+    );
+    await ProxyResponseHandler.dispatch(session, response);
+    await drainAsyncTasks();
+
+    expect(dbCosts[0]).toBe("32");
+  });
 });
 
 describe("价格表缺失/查询失败:不计费放行", () => {

+ 60 - 0
tests/unit/actions/model-prices.test.ts

@@ -7,6 +7,7 @@ const revalidatePathMock = vi.fn();
 
 // Repository mocks
 const findLatestPriceByModelMock = vi.fn();
+const findLatestPriceByModelAndSourceMock = vi.fn();
 const findAllLatestPricesMock = vi.fn();
 const createModelPriceMock = vi.fn();
 const upsertModelPriceMock = vi.fn();
@@ -36,6 +37,8 @@ vi.mock("@/lib/logger", () => ({
 
 vi.mock("@/repository/model-price", () => ({
   findLatestPriceByModel: () => findLatestPriceByModelMock(),
+  findLatestPriceByModelAndSource: (...args: unknown[]) =>
+    findLatestPriceByModelAndSourceMock(...args),
   createModelPrice: (...args: unknown[]) => createModelPriceMock(...args),
   upsertModelPrice: (...args: unknown[]) => upsertModelPriceMock(...args),
   deleteModelPriceByName: (...args: unknown[]) => deleteModelPriceByNameMock(...args),
@@ -490,4 +493,61 @@ describe("Model Price Actions", () => {
       expect(createModelPriceMock).not.toHaveBeenCalled();
     });
   });
+
+  describe("pinModelPricingProviderAsManual", () => {
+    it("should pin a cloud provider pricing node as a local manual model price", async () => {
+      findLatestPriceByModelAndSourceMock.mockResolvedValue(
+        makeMockPrice(
+          "gpt-5.4",
+          {
+            mode: "responses",
+            display_name: "GPT-5.4",
+            model_family: "gpt",
+            pricing: {
+              openrouter: {
+                input_cost_per_token: 0.0000025,
+                output_cost_per_token: 0.000015,
+                cache_read_input_token_cost: 2.5e-7,
+              },
+            },
+          },
+          "litellm"
+        )
+      );
+      upsertModelPriceMock.mockResolvedValue(
+        makeMockPrice(
+          "gpt-5.4",
+          {
+            mode: "responses",
+            input_cost_per_token: 0.0000025,
+            output_cost_per_token: 0.000015,
+            cache_read_input_token_cost: 2.5e-7,
+            selected_pricing_provider: "openrouter",
+          },
+          "manual"
+        )
+      );
+
+      const { pinModelPricingProviderAsManual } = await import("@/actions/model-prices");
+      const result = await pinModelPricingProviderAsManual({
+        modelName: "gpt-5.4",
+        pricingProviderKey: "openrouter",
+      });
+
+      expect(result.ok).toBe(true);
+      expect(findLatestPriceByModelAndSourceMock).toHaveBeenCalledWith("gpt-5.4", "litellm");
+      expect(upsertModelPriceMock).toHaveBeenCalledWith(
+        "gpt-5.4",
+        expect.objectContaining({
+          mode: "responses",
+          input_cost_per_token: 0.0000025,
+          output_cost_per_token: 0.000015,
+          cache_read_input_token_cost: 2.5e-7,
+          selected_pricing_provider: "openrouter",
+          selected_pricing_source_model: "gpt-5.4",
+          selected_pricing_resolution: "manual_pin",
+        })
+      );
+    });
+  });
 });

+ 46 - 0
tests/unit/lib/cost-calculation-long-context.test.ts

@@ -0,0 +1,46 @@
+import { describe, expect, test } from "vitest";
+import { calculateRequestCost } from "@/lib/utils/cost-calculation";
+
+describe("calculateRequestCost long-context", () => {
+  test("uses long-context output pricing when total input context exceeds threshold", () => {
+    const cost = calculateRequestCost(
+      {
+        input_tokens: 250000,
+        output_tokens: 100000,
+      },
+      {
+        mode: "chat",
+        model_family: "claude-sonnet",
+        input_cost_per_token: 0.000003,
+        input_cost_per_token_above_200k_tokens: 0.000006,
+        output_cost_per_token: 0.000015,
+        output_cost_per_token_above_200k_tokens: 0.0000225,
+      },
+      1,
+      false
+    );
+
+    expect(Number(cost.toString())).toBe(3.75);
+  });
+
+  test("does not charge 1h cache long-context price when base cache creation price is missing", () => {
+    const cost = calculateRequestCost(
+      {
+        input_tokens: 250000,
+        cache_creation_1h_input_tokens: 1000,
+      },
+      {
+        mode: "chat",
+        model_family: "gpt",
+        input_cost_per_token: 0.0000025,
+        output_cost_per_token: 0.000015,
+        cache_creation_input_token_cost_above_1hr_above_272k_tokens: 0.5,
+      },
+      1,
+      false,
+      false
+    );
+
+    expect(Number(cost.toString())).toBe(0.63);
+  });
+});

+ 46 - 0
tests/unit/lib/cost-calculation-priority.test.ts

@@ -0,0 +1,46 @@
+import { describe, expect, test } from "vitest";
+import { calculateRequestCost } from "@/lib/utils/cost-calculation";
+import type { ModelPriceData } from "@/types/model-price";
+
+function makePriceData(overrides: Partial<ModelPriceData> = {}): ModelPriceData {
+  return {
+    mode: "responses",
+    input_cost_per_token: 1,
+    output_cost_per_token: 10,
+    cache_read_input_token_cost: 0.1,
+    input_cost_per_token_priority: 2,
+    output_cost_per_token_priority: 20,
+    cache_read_input_token_cost_priority: 0.2,
+    ...overrides,
+  };
+}
+
+describe("calculateRequestCost priority service tier", () => {
+  test("uses priority pricing fields when priority service tier is applied", () => {
+    const cost = calculateRequestCost(
+      { input_tokens: 2, output_tokens: 3, cache_read_input_tokens: 5 },
+      makePriceData(),
+      1,
+      false,
+      true
+    );
+
+    expect(Number(cost.toString())).toBe(65);
+  });
+
+  test("falls back to regular pricing when priority fields are absent", () => {
+    const cost = calculateRequestCost(
+      { input_tokens: 2, output_tokens: 3, cache_read_input_tokens: 5 },
+      makePriceData({
+        input_cost_per_token_priority: undefined,
+        output_cost_per_token_priority: undefined,
+        cache_read_input_token_cost_priority: undefined,
+      }),
+      1,
+      false,
+      true
+    );
+
+    expect(Number(cost.toString())).toBe(32.5);
+  });
+});

+ 146 - 0
tests/unit/lib/utils/pricing-resolution.test.ts

@@ -0,0 +1,146 @@
+import { describe, expect, test } from "vitest";
+import type { ModelPrice } from "@/types/model-price";
+import { resolvePricingForModelRecords } from "@/lib/utils/pricing-resolution";
+
+function makeRecord(
+  modelName: string,
+  priceData: ModelPrice["priceData"],
+  source: ModelPrice["source"] = "litellm"
+): ModelPrice {
+  const now = new Date("2026-03-06T00:00:00.000Z");
+  return {
+    id: Math.floor(Math.random() * 100000),
+    modelName,
+    priceData,
+    source,
+    createdAt: now,
+    updatedAt: now,
+  };
+}
+
+describe("resolvePricingForModelRecords", () => {
+  test("falls back from chatgpt to openai pricing for gpt-5.4 alias models", () => {
+    const aliasRecord = makeRecord("gpt-5.4", {
+      mode: "responses",
+      model_family: "gpt",
+      litellm_provider: "chatgpt",
+      pricing: {
+        openai: {
+          input_cost_per_token: 0.0000025,
+          output_cost_per_token: 0.000015,
+          cache_read_input_token_cost: 2.5e-7,
+        },
+        openrouter: {
+          input_cost_per_token: 0.0000025,
+          output_cost_per_token: 0.000015,
+          cache_read_input_token_cost: 2.5e-7,
+        },
+      },
+    });
+
+    const resolved = resolvePricingForModelRecords({
+      provider: {
+        id: 1,
+        name: "ChatGPT",
+        url: "https://chatgpt.com/backend-api/codex",
+      } as never,
+      primaryModelName: "gpt-5.4",
+      fallbackModelName: null,
+      primaryRecord: aliasRecord,
+      fallbackRecord: null,
+    });
+
+    expect(resolved).not.toBeNull();
+    expect(resolved?.resolvedPricingProviderKey).toBe("openai");
+    expect(resolved?.source).toBe("official_fallback");
+    expect(resolved?.priceData.input_cost_per_token).toBe(0.0000025);
+  });
+
+  test("falls back from redirected date model to alias model for provider-specific pricing", () => {
+    const datedRecord = makeRecord("gpt-5.4-2026-03-05", {
+      mode: "responses",
+      model_family: "gpt",
+      litellm_provider: "openai",
+      input_cost_per_token: 0.0000025,
+      output_cost_per_token: 0.000015,
+      cache_read_input_token_cost: 2.5e-7,
+      pricing: {
+        openai: {
+          input_cost_per_token: 0.0000025,
+          output_cost_per_token: 0.000015,
+          cache_read_input_token_cost: 2.5e-7,
+        },
+      },
+    });
+
+    const aliasRecord = makeRecord("gpt-5.4", {
+      mode: "responses",
+      model_family: "gpt",
+      litellm_provider: "chatgpt",
+      pricing: {
+        openrouter: {
+          input_cost_per_token: 0.0000025,
+          output_cost_per_token: 0.000015,
+          cache_read_input_token_cost: 2.5e-7,
+        },
+      },
+    });
+
+    const resolved = resolvePricingForModelRecords({
+      provider: {
+        id: 2,
+        name: "OpenRouter",
+        url: "https://openrouter.ai/api/v1",
+      } as never,
+      primaryModelName: "gpt-5.4-2026-03-05",
+      fallbackModelName: "gpt-5.4",
+      primaryRecord: datedRecord,
+      fallbackRecord: aliasRecord,
+    });
+
+    expect(resolved).not.toBeNull();
+    expect(resolved?.resolvedModelName).toBe("gpt-5.4");
+    expect(resolved?.resolvedPricingProviderKey).toBe("openrouter");
+    expect(resolved?.source).toBe("cloud_model_fallback");
+  });
+
+  test("prefers local manual prices over cloud multi-provider pricing", () => {
+    const manualRecord = makeRecord(
+      "gpt-5.4",
+      {
+        mode: "responses",
+        input_cost_per_token: 0.0000099,
+        output_cost_per_token: 0.0000199,
+        selected_pricing_provider: "manual-custom",
+      },
+      "manual"
+    );
+
+    const cloudRecord = makeRecord("gpt-5.4", {
+      mode: "responses",
+      pricing: {
+        openai: {
+          input_cost_per_token: 0.0000025,
+          output_cost_per_token: 0.000015,
+        },
+      },
+    });
+
+    const resolved = resolvePricingForModelRecords({
+      provider: {
+        id: 1,
+        name: "ChatGPT",
+        url: "https://chatgpt.com/backend-api/codex",
+      } as never,
+      primaryModelName: "gpt-5.4",
+      fallbackModelName: null,
+      primaryRecord: manualRecord,
+      fallbackRecord: cloudRecord,
+    });
+
+    expect(resolved).not.toBeNull();
+    expect(resolved?.source).toBe("local_manual");
+    expect(resolved?.priceData.input_cost_per_token).toBe(0.0000099);
+    expect(resolved?.resolvedPricingProviderKey).toBe("manual-custom");
+  });
+});

+ 67 - 2
tests/unit/lib/utils/special-settings.test.ts

@@ -1,6 +1,9 @@
 import { describe, expect, test } from "vitest";
 import type { SpecialSetting } from "@/types/special-settings";
-import { buildUnifiedSpecialSettings } from "@/lib/utils/special-settings";
+import {
+  buildUnifiedSpecialSettings,
+  hasPriorityServiceTierSpecialSetting,
+} from "@/lib/utils/special-settings";
 
 describe("buildUnifiedSpecialSettings", () => {
   test("无任何输入时应返回 null", () => {
@@ -72,7 +75,7 @@ describe("buildUnifiedSpecialSettings", () => {
     );
   });
 
-  test("context1mApplied=true 时应派生 anthropic_context_1m_header_override 特殊设置", () => {
+  test("context1mApplied=true 时应在无显式记录时回退派生 anthropic_context_1m_header_override", () => {
     const settings = buildUnifiedSpecialSettings({
       existing: null,
       context1mApplied: true,
@@ -91,6 +94,26 @@ describe("buildUnifiedSpecialSettings", () => {
     );
   });
 
+  test("context1mApplied=true 但已有 codex service tier result 时不应回退派生 anthropic_context_1m_header_override", () => {
+    const settings = buildUnifiedSpecialSettings({
+      existing: [
+        {
+          type: "codex_service_tier_result",
+          scope: "response",
+          hit: true,
+          requestedServiceTier: "priority",
+          actualServiceTier: null,
+          effectivePriority: true,
+        },
+      ],
+      context1mApplied: true,
+    });
+
+    expect(settings?.some((item) => item.type === "anthropic_context_1m_header_override")).toBe(
+      false
+    );
+  });
+
   test("应合并 existing specialSettings 与派生 specialSettings", () => {
     const existing: SpecialSetting[] = [
       {
@@ -169,3 +192,45 @@ describe("buildUnifiedSpecialSettings", () => {
     expect(settings?.filter((s) => s.type === "guard_intercept").length).toBe(1);
   });
 });
+
+describe("hasPriorityServiceTierSpecialSetting", () => {
+  test("returns true when codex actual service tier is priority", () => {
+    expect(
+      hasPriorityServiceTierSpecialSetting([
+        {
+          type: "codex_service_tier_result",
+          scope: "response",
+          hit: true,
+          requestedServiceTier: "default",
+          actualServiceTier: "priority",
+          effectivePriority: true,
+        },
+      ])
+    ).toBe(true);
+  });
+
+  test("returns false when codex actual service tier is non-priority even if request was priority", () => {
+    expect(
+      hasPriorityServiceTierSpecialSetting([
+        {
+          type: "provider_parameter_override",
+          scope: "provider",
+          providerId: 1,
+          providerName: "p",
+          providerType: "codex",
+          hit: true,
+          changed: true,
+          changes: [{ path: "service_tier", before: null, after: "priority", changed: true }],
+        },
+        {
+          type: "codex_service_tier_result",
+          scope: "response",
+          hit: true,
+          requestedServiceTier: "priority",
+          actualServiceTier: "default",
+          effectivePriority: false,
+        },
+      ])
+    ).toBe(false);
+  });
+});

+ 32 - 4
tests/unit/proxy/pricing-no-price.test.ts

@@ -1,12 +1,15 @@
 import { beforeEach, describe, expect, it, vi } from "vitest";
+import type { ModelPriceData } from "@/types/model-price";
 import type { SystemSettings } from "@/types/system-config";
 
-const { cloudSyncRequests, requestCloudPriceTableSyncMock } = vi.hoisted(() => {
-  const cloudSyncRequests: Array<{ reason: string }> = [];
+const asyncTasks: Promise<void>[] = [];
+const cloudSyncRequests: Array<{ reason: string }> = [];
+
+const { requestCloudPriceTableSyncMock } = vi.hoisted(() => {
   const requestCloudPriceTableSyncMock = vi.fn((payload: { reason: string }) => {
     cloudSyncRequests.push(payload);
   });
-  return { cloudSyncRequests, requestCloudPriceTableSyncMock };
+  return { requestCloudPriceTableSyncMock };
 });
 
 vi.mock("@/lib/price-sync/cloud-price-updater", () => ({
@@ -64,7 +67,7 @@ vi.mock("@/lib/proxy-status-tracker", () => ({
 import { finalizeRequestStats } from "@/app/v1/_lib/proxy/response-handler";
 import { ProxySession } from "@/app/v1/_lib/proxy/session";
 import { RateLimitService } from "@/lib/rate-limit";
-import { updateMessageRequestCost } from "@/repository/message";
+import { updateMessageRequestCost, updateMessageRequestDetails } from "@/repository/message";
 import { findLatestPriceByModel } from "@/repository/model-price";
 import { getSystemSettings } from "@/repository/system-config";
 
@@ -182,6 +185,31 @@ describe("价格表缺失/查询失败:请求不计费且不报错", () => {
     vi.clearAllMocks();
   });
 
+  it("无 usageMetrics 时仍应写入 request details", async () => {
+    vi.mocked(getSystemSettings).mockResolvedValue(makeSystemSettings("redirected"));
+    vi.mocked(findLatestPriceByModel).mockResolvedValue(null);
+
+    const session = createSession({ originalModel: "m1", redirectedModel: "m2" });
+    await finalizeRequestStats(
+      session,
+      JSON.stringify({ type: "message", usage: null }),
+      502,
+      9,
+      "bad upstream"
+    );
+
+    expect(updateMessageRequestDetails).toHaveBeenCalledWith(
+      2000,
+      expect.objectContaining({
+        statusCode: 502,
+        errorMessage: "bad upstream",
+        model: "m2",
+        providerId: 99,
+      })
+    );
+    expect(updateMessageRequestCost).not.toHaveBeenCalled();
+  });
+
   it("无价格:应跳过 DB cost 更新与限流 cost 追踪,并触发异步同步", async () => {
     vi.mocked(getSystemSettings).mockResolvedValue(makeSystemSettings("redirected"));
     vi.mocked(findLatestPriceByModel).mockResolvedValue(null);

+ 34 - 3
tests/unit/proxy/session.test.ts

@@ -385,15 +385,46 @@ describe("ProxySession.getCachedPriceDataByBillingSource", () => {
     expect(findLatestPriceByModel).toHaveBeenCalledTimes(1);
   });
 
-  it("应在无模型时返回 null", async () => {
+  it("应缓存 resolved pricing 避免重复查询", async () => {
+    const redirectedPriceData: ModelPriceData = {
+      mode: "responses",
+      model_family: "gpt",
+      pricing: {
+        openai: {
+          input_cost_per_token: 3,
+          output_cost_per_token: 4,
+        },
+      },
+    };
+
     vi.mocked(getSystemSettings).mockResolvedValue(makeSystemSettings("redirected"));
+    vi.mocked(findLatestPriceByModel).mockResolvedValue(
+      makePriceRecord("redirected-model", redirectedPriceData)
+    );
+
+    const session = createSession({
+      originalModel: "original-model",
+      redirectedModel: "redirected-model",
+    });
+
+    const provider = {
+      id: 77,
+      name: "ChatGPT",
+      url: "https://chatgpt.com/backend-api/codex",
+      providerType: "codex",
+    } as any;
+
+    await session.getResolvedPricingByBillingSource(provider);
+    await session.getResolvedPricingByBillingSource(provider);
+
+    expect(findLatestPriceByModel).toHaveBeenCalledTimes(1);
+  });
 
+  it("无模型时应返回 null", async () => {
     const session = createSession({ redirectedModel: null });
     const result = await session.getCachedPriceDataByBillingSource();
 
     expect(result).toBeNull();
-    expect(getSystemSettings).not.toHaveBeenCalled();
-    expect(findLatestPriceByModel).not.toHaveBeenCalled();
   });
 });
 

+ 81 - 0
tests/unit/settings/prices/price-list-multi-provider-ui.test.tsx

@@ -0,0 +1,81 @@
+/**
+ * @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 } from "vitest";
+import { PriceList } from "@/app/[locale]/settings/prices/_components/price-list";
+import type { ModelPrice } from "@/types/model-price";
+import { loadMessages } from "./test-messages";
+
+function render(node: ReactNode) {
+  const container = document.createElement("div");
+  document.body.appendChild(container);
+  const root = createRoot(container);
+
+  act(() => {
+    root.render(node);
+  });
+
+  return {
+    unmount: () => {
+      act(() => root.unmount());
+      container.remove();
+    },
+  };
+}
+
+describe("PriceList multi-provider pricing", () => {
+  test("renders a Multi badge when a model contains multiple provider pricing nodes", () => {
+    const messages = loadMessages();
+    const now = new Date("2026-03-06T00:00:00.000Z");
+
+    const prices: ModelPrice[] = [
+      {
+        id: 1,
+        modelName: "gpt-5.4",
+        priceData: {
+          mode: "responses",
+          display_name: "GPT-5.4",
+          model_family: "gpt",
+          litellm_provider: "chatgpt",
+          pricing: {
+            openai: {
+              input_cost_per_token: 0.0000025,
+              output_cost_per_token: 0.000015,
+            },
+            openrouter: {
+              input_cost_per_token: 0.0000025,
+              output_cost_per_token: 0.000015,
+            },
+          },
+        },
+        source: "litellm",
+        createdAt: now,
+        updatedAt: now,
+      },
+    ];
+
+    const { unmount } = render(
+      <NextIntlClientProvider locale="en" messages={messages}>
+        <PriceList
+          initialPrices={prices}
+          initialTotal={prices.length}
+          initialPage={1}
+          initialPageSize={50}
+          initialSearchTerm=""
+          initialSourceFilter=""
+          initialLitellmProviderFilter=""
+        />
+      </NextIntlClientProvider>
+    );
+
+    expect(document.body.textContent).toContain("Multi");
+    expect(document.body.textContent).toContain("$2.50/M");
+    expect(document.body.textContent).toContain("$15.00/M");
+    unmount();
+  });
+});