FR-122.12-D · CM-2339 · 討論稿 · 2026-09-29

原廠金鑰的 AI 花費:每租戶、每人一個美金額度,超過只擋 AI

第一階段已把每次 AI 呼叫花多少美金記進 ai_call_log。這一稿講怎麼「管錢」:租戶與使用者各一個美金額度(日或月)、每分鐘次數改由閘道統一管、一頁看時間區間內的花費。全部已裁:9 項照寫,D-a、D-f~D-j 決策者 2026-09-29 裁定全照稿內建議。定案已落 design.md §3.2、§5.18、§6.3、§7.3。

已裁:美金計、不做點數 已裁:額度直接加總紀錄表 已裁:license 這版只預留 D-a、D-f~D-j 照建議定案 定案見 design.md §5.18
§1

先看 mockup

開啟 mockup:設定區塊/AI 用量頁/額度用完時的畫面(深色主題、配色照 FE design tokens,三個分頁切換)

§2

分工概述(30 秒版)

一句話:用原廠金鑰打 AI 的花費,每個租戶一天(或一個月)最多花多少美金、每個人最多花多少美金、每分鐘最多送幾次,超過就只擋 AI;另外給一頁看「這段期間花了多少、誰花的」。

階段(子需求) 做什麼 產出 完成怎麼判定(決策者親手檢查法)
FR-122.12 額度與次數 閘道每次打 AI 前先算「本期已花多少」,超過就擋;每分鐘次數從小幫手、儀表板各自的計數搬進閘道統一算,分類也一起管 閘道套件的額度檢查點改版、主專案的額度實作、分類送出前預估 在資料庫把自己的每人額度改成 0.01 美金 → 小幫手問兩句,第二句回「今天額度已用完」;儀表板、分類同樣被擋;打開既有儀表板與分類結果照常看得到
FR-122.13 設定頁 「AI 服務設定」頁加一塊「AI 使用額度」:兩個金額+週期、每分鐘次數、本期已用進度條;AI 功能頁到 80% 出黃條 設定 API+前端區塊+黃條 用租戶管理員登入,把租戶額度設 0.05 美金/日並儲存 → 重新整理數字還在;用小幫手聊到進度條過 80% → 小幫手視窗頂端出現黃條
FR-122.14 用量頁 新一頁「AI 用量」:選期間、依租戶/使用者/功能/模型/日期分組看花費,可點進現有的呼叫紀錄 彙總 API+前端頁 選「本月、依使用者」→ 每人一列金額;把各列加起來=頁頂總額;點某人「明細」跳到呼叫紀錄且只剩他的紀錄

依賴:.12 是地基(額度怎麼算、存在哪);.13 的進度條與 .14 的彙總都讀 .12 定下的設定與算法,可在 .12 的設定格式定案後平行開工。決策者功課:裁 D-a 預設數字(其他待裁項都有建議,照建議即可開工)。

§3

已裁(2026-09-29,不再討論)

額度單位=美金,推翻 09-28 的「點數」

每筆呼叫的 estimated_usd(閘道算好的美金估價)已落在 ai_call_log,直接拿來加總。不做點數換算表。

兩層額度,任一層超就擋

每租戶一個上限+每使用者一個上限(全租戶同一個數,不逐人設)。與 LiteLLM、Bifrost 同做法。每個上限=「金額+週期」,週期下拉(日/月),預設「日」。

週期邊界:日曆日、伺服器本地時區、零點重算;月=日曆月

不做「滾動 24 小時」。伺服器時區是安裝時的 TZ(預設 Asia/Taipei,見 docker/production/docker-compose.yml)。理由:畫面上「明天 00:00 恢復」一句話講得清楚,滾動制要講「最早那筆滿 24 小時後逐步恢復」,使用者看不懂。

額度直接加總 ai_call_log,不另建 Redis 計數器

每次呼叫前 SUM(estimated_usd)(只算原廠金鑰、當期範圍)。理由與反悔條件見資料模型 §算法。每分鐘次數維持 Redis(沿用既有 RedisRateLimiter),搬進閘道。

幾個請求同時進來、超過一點點可以接受

加總後才送 AI,同一瞬間多筆都看到「還沒超」就都會放行。接受,不上鎖。

證據分類送出前先預估,明顯不夠就整批先擋

用該租戶過去分類每份的平均花費 × 份數估算,明顯超過剩餘額度就不讓送出並說明(見預估)。

設定放在「AI 服務設定」頁,每租戶可不同,ROOT 那筆=全系統預設

租戶管理員設自己租戶;租戶沒設的欄位繼承 ROOT(tenant 1)那筆。預設要保守,不是「沒設就無上限」。

license 額度這版不做、只預留

落地版客戶可以自帶 API 金鑰,額度只管原廠金鑰的試用用量,不需要原廠用授權檔鎖。第一版設定存 system_configs;日後 SaaS 版由 license limits 當「上限的上限」,欄位名現在先對齊(見資料模型)。

沿用不變

只算走原廠鑰的用量、客戶自帶金鑰不計;超上限只擋 AI 呼叫,其他功能與歷史結果照常;80% 告警只在畫面(落地版沒有 email);花費頁獨立一頁,系統管理員看全部、租戶管理員看自己。

§4

市面方案盤點

方案 額度怎麼定 分幾層 超了怎樣 我們能不能直接用
LiteLLM proxy max_budget(美金)+budget_duration(如 1d、30d);另有 rpm_limit(每分鐘次數) User/Team/Key 三層,任一超就擋 擋下;另有 soft_budget 軟上限只告警 不能直接用:預算功能只在 proxy 模式,我們依 D9 用 library 形式(行程內 import litellm),沒有 proxy 那層;而且它不認得「原廠鑰/客戶鑰」之分
Bifrost max_limit+reset_duration,可對齊日曆日 Customer/Team/VirtualKey/Provider 四層 額度超回 402、次數超回 429 帶 Retry-After 同上,是另一個閘道服務;要多一個容器,且與我們自己的守門、稽核管線重疊

抄什麼:「金額+週期」的欄位形狀、多層任一超就擋、次數用 429+還要等幾秒、軟上限只告警不擋。不抄什麼:多一個 proxy 服務。我們已有自己的閘道與 ai_call_log,額度是在閘道上多一道檢查,不是再疊一層。

來源:https://docs.litellm.ai/docs/proxy/users、https://docs.litellm.ai/docs/proxy/team_budgets、https://docs.getbifrost.ai/features/governance/budget-and-limits

§5

現況盤點(可複用 vs 缺口)

以下每一列都已開檔核對。

元件 現況 本案動作
閘道的額度檢查點 IQuotaCounter(jedi-ai-gateway/domain/ports.py:44) 有 check(tenant_id, feature) 與 consume(...),主專案接的是不限額的 UnlimitedQuota 可複用但要改介面:①check 只收租戶,拿不到使用者,每人額度做不了;②check 在管線第 0 步(gateway_service.py:126),早於第 ③ 步解析金鑰,那時還不知道這次走原廠鑰還是客戶鑰,客戶自帶金鑰的租戶用完原廠額度也會被擋。見呼叫時序
擋下的例外 GatewayBlockedError.QUOTA 已有這個原因代碼;小幫手已有對應訊息「本期 AI 使用額度已用完」(ai_bot_service.py:55) 複用;另加 RATE_LIMIT 原因代碼
每分鐘次數:小幫手 套件寫死 DEFAULT_RATE_LIMIT_PER_MINUTE = 10(ai_bot_service.py:44),每人一分鐘 10 次 退場,改由閘道管
每分鐘次數:儀表板 套件寫死 3 次(jedi_ai_dashboard/plugin/contract.py:24,不是 10;一次生成打兩次 AI 所以較嚴) 退場,改由閘道管
每分鐘次數:分類 沒有限制;一批並行 4 份(gateway_classifier.py DEFAULT_CONCURRENCY = 4),DEV 實測 63 份跑 3.5 分鐘,約每分鐘 18 份 納入閘道;每分鐘上限會拉長大批分類的時間,見 D-a
計次實作 RedisRateLimiter(common/util/redis_rate_limiter.py) 固定視窗 Redis 計數,Redis 掛掉就放行;兩支 AI 套件的插槽共用 原樣複用,改接到閘道
429 回應(core/app_factory.py:483) 兩支套件的「太頻繁」例外轉 429 帶 retry_after 保留;閘道擋下的次數超過走各功能既有的「被擋」通道(見回應)
設定表 system_configs 每租戶一列一組 group/key;AI_PROVIDER_CONFIG 群組鎖平台管理員(AI_PROVIDER_CONFIG_PLATFORM_ADMIN_ONLY = True),同群組已有 CALL_LOG_RETENTION、AI_CALL_LOG_RETENTION_DAYS 兩個 key 額度不能放進 AI_PROVIDER_CONFIG 群組——整組鎖平台管理員,租戶管理員會改不到。另開群組 AI_QUOTA
租戶→ROOT 讀法 read_tenant_config_value(tenant_id, group, key)(繞 RLS、顯式指定租戶),resolve_retention() 已是「租戶→ROOT→預設」的現成樣板 照抄這個樣板,不另寫讀法
新租戶抄設定 tenant_storage_config_seeder._INHERITED_CONFIGS 建租戶時把 ROOT 的 STORAGE_CONFIG、AI_PROVIDER_CONFIG 複製一份給新租戶 🔴 額度不可加進這張清單:抄了就變成「建租戶當下的快照」,之後改 ROOT 預設不會帶到舊租戶,與「沒設就繼承」相反
ai_call_log 有 tenant_id、user_id、feature、model、key_source、estimated_usd、status、created_at;索引已有 (tenant_id, created_at DESC) 與 (user_id, created_at DESC) 加總用現成欄位;(tenant_id, created_at) 索引已存在(scripts/sql/2026-09-28-fr122-ai-call-log.sql:65),不必再加
AI 呼叫紀錄頁 已上線,能力點 ai-call-log.read(租戶管理員也有),AiCallLogQueryEntity 可篩 feature/status/user/日期 用量頁複用這顆能力點;下鑽直接帶條件跳過去
license limits(common/authz/license.py) 現只用 max_sub_tenants 這版不動,只對齊欄位名預留

DEV 實測(本機 guidant_ai_dev,356 筆):原廠鑰成功呼叫 306 筆全有美金估價;失敗 3 筆、被擋 47 筆估價為空(沒真的打到 AI,本來就不該計錢)。每次平均:小幫手 0.0015、儀表板挑圖 0.005+排版 0.028(一次生成約 0.034)、分類每份 0.016 美金。今天花最多的一位測試帳號一天 3.68 美金。加總查詢實測走索引 0.1 毫秒。

§6

資料模型草案

設定存哪

system_configs 新群組 AI_QUOTA,兩個 key:

group / key 誰能改 內容
AI_QUOTA / CONFIG 租戶管理員改自己租戶;平台管理員改 ROOT(=全系統預設) 這個租戶實際生效的額度
AI_QUOTA / CEILING 只有平台管理員(D-h) 平台給這個租戶的上限;沒填=上限就是 ROOT 的 CONFIG

CONFIG 的值(JSONB):

{
  "tenant_budget": {"usd": 5.00, "period": "day"},
  "user_budget":   {"usd": 1.00, "period": "day"},
  "user_rpm": 20,
  "warn_ratio": 0.8
}
  • period 只能是 day/month。
  • usd 填 0 =這一層不准用原廠鑰;不提供「無上限」(已裁:預設要保守)。平台管理員要給某租戶很大的額度,就填一個大數字。
  • warn_ratio 先寫死 0.8,不在畫面上開。

🔵 預留(日後 SaaS):license limits 將帶同一個形狀 {"ai_quota": {"tenant_budget": …, "user_budget": …, "user_rpm": …}},當作「上限的上限」——生效值=min(授權檔, CEILING, CONFIG)。這版不讀 license,只把欄位名定成一樣,日後接上不必轉換。

繼承規則

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart TB
  CODE["程式內建預設<br/>(ROOT 也沒設時的最後防線)"] --> ROOT
  ROOT["ROOT(tenant 1)AI_QUOTA/CONFIG<br/>=全系統預設"] --> CEIL
  CEIL["該租戶 AI_QUOTA/CEILING<br/>平台管理員設;沒填=ROOT 的值"] --> TEN
  TEN["該租戶 AI_QUOTA/CONFIG<br/>租戶管理員設;沒填=上限值"] --> EFF
  EFF["生效額度<br/>=min(上限, 租戶設定)"] --> T1["租戶層:本租戶全部人加總"]
  EFF --> U1["使用者層:每個人各自算<br/>(同一個數,不逐人設)"]
  EFF --> R1["每分鐘次數:每個人各自算"]
  LIC["license limits.ai_quota<br/>(預留,這版不讀)"] -. 日後 SaaS .-> CEIL
圖 1 — 設定繼承:每個欄位各自往上找;租戶只能調低
  • 逐欄位往上找:租戶只填了租戶額度,每人額度與每分鐘次數照樣繼承上一層。
  • 讀法照抄 resolve_retention()(租戶→ROOT→預設),用 read_tenant_config_value。
  • 子租戶:只往 ROOT 繼承,不往父租戶繼承、也不把子租戶花費算進父租戶(D-i)。

算法:每次呼叫前加總(已裁)

-- 租戶層(使用者層多一個 AND user_id = :uid)
SELECT COALESCE(SUM(estimated_usd), 0)
  FROM public.ai_call_log
 WHERE tenant_id = :tid
   AND key_source IN ('root' /* , 'env' 見 D-j */)
   AND created_at >= :period_start;   -- 本地零點或月初,Python 端依 TZ 算好再傳入
  • 為什麼可行:索引 (tenant_id, created_at DESC) 已在,一天的量是幾百筆等級,實測 0.1 毫秒;每次 AI 呼叫本身要數秒,多這一句看不出來。
  • 為什麼不用 Redis 計數器:計數器要處理「零點歸零」「和紀錄表對不起來」「Redis 重啟歸零=額度被重置」三件事,換來的只是省一句已經很快的查詢。
  • 🔴 期間起點在 Python 端算好再傳,不要寫 date_trunc('day', now()):那是資料庫連線的時區,與應用容器的 TZ 不一定一樣,兩邊差 8 小時的症狀是「每天早上 8 點才歸零」,而且不會報錯。
  • 讀法走「顯式指定租戶、繞 RLS」的 infra 查詢(與 read_tenant_config_value 同一個樣板),因為分類的背景工作沒有請求脈絡。
  • 🔴 紀錄保留天數與月額度的交互:AI_CALL_LOG_RETENTION_DAYS 若被設成 31 天以下,月初的紀錄會先被清掉,月額度就少算。設定頁要擋「週期選月、保留天數 < 31」。
  • 反悔條件:單一租戶日呼叫量上萬筆,或 SaaS 版多租戶共用一個資料庫、加總開始拖慢呼叫時,在 IQuotaCounter 的實作層換成 Redis 計數器+每日對帳,閘道介面不動。

閘道介面要改的地方(碰 jedi-ai-gateway 套件)

改動 為什麼
IQuotaCounter.check(ctx: CallContext, feature, key_source),取代只收 tenant_id 要拿到 user_id 才能算每人額度;要知道這次走哪把鑰才能「客戶自帶鑰不計也不擋」
額度檢查從第 0 步移到第 ③ 步(解析金鑰)之後 第 0 步還不知道金鑰來源
新增 IRateLimiter port 與 GatewayBlockedError.RATE_LIMIT(帶 retry_after) 每分鐘次數搬進閘道;插槽形狀與現有 RedisRateLimiter.acquire(key, limit, window) 一樣,主專案原樣接上
規則檔每個 feature 加 counts_rate: true/false 儀表板一次生成打兩次 AI(挑圖+排版),只算挑圖那次,才對得上「生成一次=1 次」
consume() 在加總法下不做事 花費由稽核落表那一筆就是來源;保留介面給日後換 Redis 用

依 CLAUDE.md 外部套件規範,這張改動要在實作卡的計畫裡列明、等決策者點頭;其他 consumer 只有主專案自己(三支 AI 功能都經主專案接線),風險低。

§7

呼叫時序

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
  participant F as 功能(小幫手/儀表板/分類)
  participant G as 閘道
  participant R as Redis 計次
  participant Q as 額度檢查(主專案)
  participant DB as ai_call_log
  participant AI as AI 供應商
  F->>G: complete(請求, 誰, 哪個功能)
  G->>R: 這個人這分鐘第幾次?(counts_rate 的功能才算)
  alt 超過每分鐘次數
    R-->>G: 還要等 N 秒
    G-->>F: 擋下 RATE_LIMIT(背景分類:等 N 秒後自動重試)
  end
  G->>G: ① 入口守門 ② 組訊息 ③ 解析金鑰(得知原廠鑰/客戶鑰)
  alt 客戶自帶金鑰
    Note over G,Q: 不查額度、不計
  else 原廠金鑰
    G->>Q: check(誰, 功能, 金鑰來源)
    Q->>DB: 加總本期 租戶花費、本人花費
    alt 任一層 ≥ 額度
      Q-->>G: 擋下 QUOTA(附哪一層、何時恢復)
      G->>DB: 記一筆 status=blocked
      G-->>F: 擋下,只擋這次 AI;其他功能照常
    end
  end
  G->>AI: ④ 送出
  AI-->>G: 回覆+用量
  G->>G: ⑤ 出口守門
  G->>DB: ⑥ 記一筆(含 estimated_usd)
  G-->>F: 結果
  Note over F,DB: 80% 告警不在這條路上算——畫面讀「本期已用」時才比(見 80% 告警)
圖 2 — 一次 AI 呼叫:先算次數、解析金鑰、再算額度;落表後下次加總就算得到
§8

每分鐘次數搬進閘道

功能 現在 之後
小幫手 套件內每人 10 次/分,主專案 core/plugins/ai_bot.py:93 傳 RedisRateLimiter() 進去 主專案不再傳 rate_limiter(套件在沒接時本來就不限),改由閘道算。套件程式不必改
儀表板 套件內每人 3 次/分,core/plugins/ai_dashboard.py:97 同上 同上;閘道只算「挑圖」那次,排版不算
分類 不限 閘道每份檔算 1 次;在背景工作行程被擋時不報錯,等 N 秒後重試(閘道看 executor=worker 決定等待,最多等一個視窗)
資源庫的租戶級節流(assert_within_tenant_limit,resource_library_app_service.py:470) 另一件事(防灌爆框架解析),不打 AI 不動

計數的 key 用 ai_rate:{tenant_id}:{user_id},數值取生效設定的 user_rpm。Redis 掛掉照現有做法放行並記 warning。

兩個套件內寫死的上限值(10、3)退場後變成沒人用的預設值,留在套件裡不影響行為;下次動這兩支套件時順手刪掉。

§9

額度超/次數超的回應與畫面

三支功能收到閘道擋下時,本來就各有「被擋」的呈現通道,不必新造:

功能 現有被擋通道(已開檔核對) 額度用完 次數超過
小幫手 回 200,ChatReply.blocked=原因,reply 是給人看的句子(ai_bot_service.py:207) 聊天泡泡:「你今天的 AI 額度(1.00 美金)已用完,明天 00:00 恢復。」 聊天泡泡:「送出太頻繁,請 23 秒後再試。」
儀表板 AIGenerationResult(success=False, error_kind=BLOCKED, message=exc.reply)(ai_dashboard_app_service.py:348) 生成區塊顯示同樣句子 同左
分類 每份檔的 row["error"] 記下錯誤繼續下一份(gateway_classifier.py:231) 分類跑到一半用完:已分類的保留,後面的檔不再逐份打閘道(每份都會被擋又多一筆紀錄),直接標「額度用完未分類」,明天可重跑 背景等待重試,使用者看不到
分類送出前預估 無(同步 API) 明顯不夠 → 整批不受理,回錯誤碼(D-f) —

句子由閘道放進 GatewayBlockedError.reply,兩支 AI 套件都已經「有 reply 就原樣回」,所以套件不必改就能講清楚是哪一層、何時恢復。

§10

80% 告警怎麼算、怎麼顯示

  • 不存告警事件、不另算:畫面要顯示時,呼叫一支「我目前的額度」API,回傳租戶層與本人層的「已用/額度/重置時間」,前端自己比 80%。
  • 出現在哪:小幫手視窗頂端、動態儀表板生成區、證據分類送出鈕旁,黃條一行字(mockup 第③頁)。租戶管理員多一個「調整額度」連結。
  • 何時重讀:進頁面時一次、每次 AI 呼叫完一次。不輪詢。
  • 客戶自帶金鑰的租戶:API 回「不計額度」,畫面不顯示黃條也不顯示進度條。
§11

分類送出前預估

  • 估法:該租戶近 30 天分類每份的平均花費 × 這批份數;租戶沒有歷史就用全系統平均;再沒有就用常數 0.02 美金/份。
  • 「明顯不夠」=預估 > 剩餘額度 × 1.5(租戶層、本人層都比)。差一點的照送,跑到一半用完照上面處理。1.5 這個數字放進 D-a 一起裁。
  • 擋下時的說法:「這批 67 份預估約 1.05 美金,你今天只剩 0.40 美金,請減少到約 25 份以內或明天再送。」(mockup 第③頁)
§12

用量頁

項目 內容
篩選 期間(本日/本週/本月/自訂)、分組(租戶/使用者/功能/模型/日期)、金鑰來源(原廠鑰/全部,預設原廠鑰)、租戶(只有平台管理員看得到這個篩選)
頂部三格 花費(美金)、呼叫次數、被擋下次數(額度+次數)
表格欄 分組名稱、呼叫次數、輸入 token、輸出 token、花費、占比、「明細 →」
下鑽 帶同樣期間與條件跳到現有「AI 呼叫紀錄」頁
匯出 沿用紀錄頁的 xlsx 匯出做法(ai_call_log_export.py)
權限 沿用 ai-call-log.read;租戶管理員靠 RLS 只看到自己租戶,平台管理員看到全部
查詢 GROUP BY 分組欄位 over ai_call_log,期間與租戶走既有索引
  • 使用者分組要同時回 user_name(暱稱),照 CLAUDE.md 審計欄位規範在 app service 批次查。
  • 功能分組把 ai-dashboard.select/ai-dashboard.layout 合成一列「動態儀表板」,可展開看兩段。
§13

設定頁 mockup 說明

見 mockup 第①頁。重點:

  • 放在現有「AI 服務設定」頁(AiServiceConfigForm.vue,253 行,加一塊不會超過 800 行上限;建議新區塊拆成獨立子元件)。
  • 🔴 頁面權限要拆開:現在整頁鎖平台管理員(AI_SERVICE_CONFIG_PLATFORM_ADMIN_ONLY,路由、選單、頁面三處同引)。額度區塊要讓租戶管理員進得來、但金鑰區塊仍只有平台管理員看得到——路由與選單改成「有 storage-config.read 就進得來」,頁內金鑰區塊照舊用那個旗標藏起來。
  • 沒填的欄位用灰字顯示繼承來的值;「還原為系統預設」=清掉本租戶這一列。
  • 輸入超過上限時,存檔被擋並說明「平台給本租戶的上限是 20.00 美金/日」。
  • 用 PrimeVue InputNumber(兩位小數)+ Dropdown(日/月)+ ProgressBar;色彩用 design tokens,80% 以上用 --color-orange、滿了用 --color-red。
§14

待裁(2026-09-29 已全部照建議裁定)

以下各項決策者已裁定照「我的建議」走,保留原文供查理由。原列的 D-b(license 關係)、D-c(加總或計數器)、D-d(週期邊界)、D-e(分類預估)已由決策者裁定,見已裁。

D-a 預設值

ROOT 那筆出廠時的數字(租戶沒設就用它):

項目 建議 換算成實際用量(DEV 實測單價)
租戶額度 5 美金/日 小幫手約 3,300 句,或儀表板約 145 次,或分類約 310 份
每人額度 1 美金/日 小幫手約 660 句,或儀表板約 29 次,或分類約 62 份
每人每分鐘次數 20 次 小幫手 10 次太緊不會碰到;分類現在一分鐘約 18 份,設 10 會讓 67 份那批從 3.5 分鐘拉長到約 7 分鐘
分類預估「明顯不夠」倍數 1.5 倍 —

我的建議:照上表。卡上原提的 10 次/分鐘會拖慢分類一倍,改 20;若寧可慢也要保守,就維持 10。

D-f 額度用完回什麼狀態碼

  • A(建議) 小幫手與儀表板沿用現有「被擋」通道(200 內帶 blocked=quota 與說明句);只有「分類送出前預估不夠」這支同步 API 回錯誤,用 412 前置條件不滿足+新錯誤碼 GRC_412xxx(照專案命名規則)。
  • B 仿 Bifrost 回 402:專案例外類別表沒有 402,jedi_common 也沒有對應類別,要新造;而小幫手與儀表板的被擋本來就不是 HTTP 錯誤,改成 402 等於改兩支套件的回應契約。

我的建議:A。

D-g 用量頁要不要跟現有「AI 呼叫紀錄」頁合併

  • A(建議) 合併成一頁兩分頁:「用量彙總」(新)+「呼叫明細」(現有頁原封搬進來);下鑽就是切分頁並帶條件。選單只有一個入口「AI 用量」。
  • B 各自一頁:選單多一項,下鑽要跨頁帶參數。

我的建議:A。兩者同一顆能力點、同一張表,讀者要的是「先看總數、再點進去看是哪幾筆」。

D-h 租戶管理員能不能把額度調高過系統預設(新增)

租戶管理員自己設額度,但花的是原廠的錢。不設上限的話,租戶管理員填 1,000 美金/日就等於沒有額度。

  • A(建議) 租戶只能在「上限」以內調整;上限預設=ROOT 的系統預設,平台管理員可以對個別租戶另設上限(AI_QUOTA/CEILING,只有平台管理員能改)。日後 SaaS 的 license 額度接在同一個位置。
  • B 租戶愛設多少設多少:最簡單,但額度形同虛設。
  • C 只有平台管理員能設,租戶管理員只能看:與已裁「租戶管理員設自己租戶」衝突。

我的建議:A。

D-i 子租戶怎麼算(新增)

  • A(建議) 每個租戶各算各的:子租戶的設定只往 ROOT 繼承,花費也不加到父租戶頭上。與現有金鑰、保留天數的「租戶→ROOT」讀法一致。
  • B 父租戶額度涵蓋全部子租戶加總:要數整棵樹(count_descendants 那類查法),加總查詢變重,且子租戶管理員看不到兄弟租戶花了多少,無從理解為什麼被擋。

我的建議:A;若日後要「整個客戶一個總額度」,在 SaaS 的 license 額度那層做。

D-j 金鑰來源是「環境變數」的算不算原廠鑰(新增)

金鑰解析有三層:租戶自己的、ROOT 的(root)、環境變數的(env)。環境變數那把也是平台方的鑰,只是沒填在畫面上。

  • A(建議) root 與 env 都計入額度;只有 tenant(客戶自己填的)不計。
  • B 只算 root:部署時用環境變數放原廠鑰的機器就完全沒有額度。

我的建議:A。

§15

拆卡草案

子需求 範圍 依賴 驗收條件
FR-122.12 額度與次數 ①閘道套件:IQuotaCounter.check 改收 CallContext+金鑰來源、檢查移到第 ③ 步後、新增 IRateLimiter port 與 RATE_LIMIT 原因、規則檔 counts_rate、背景工作被次數擋時等待重試(要決策者先點頭動套件)②主專案:AI_QUOTA 設定讀取(租戶→上限→ROOT→內建)、加總查詢、QuotaCounter 與次數實作接線、兩支 AI 套件不再傳 rate_limiter ③分類:送出前預估、用完後剩餘檔標「額度用完未分類」 無 ①每人額度設 0.01 → 小幫手第二句被擋、說明「明天 00:00 恢復」、ai_call_log 多一筆 status=blocked;②同租戶改用自己的金鑰 → 不被擋、不計入;③每分鐘次數設 2 → 第 3 句回「請 N 秒後再試」;④儀表板生成一次只算 1 次;⑤分類 67 份、額度只夠 10 份 → 送出前被擋並說明;額度夠 60 份時送出,約 60 份有結果、其餘標「額度用完未分類」,且 ai_call_log 沒有 7 筆以上的連續被擋紀錄;⑥把伺服器 TZ 設 Asia/Taipei,23:59 用完、00:00 後可再用
FR-122.13 設定頁 ①BE:AI_QUOTA 讀寫 API(CONFIG 租戶管理員、CEILING 平台管理員;存檔檢查不超過上限、月週期時保留天數 ≥ 31)、「我目前的額度」API ②FE:AI 服務設定頁拆權限(額度區塊租戶管理員可進、金鑰區塊仍鎖平台)、額度區塊子元件、三支 AI 功能頁 80% 黃條 ③不把 AI_QUOTA 加進新租戶抄設定清單 .12 的設定格式 ①租戶管理員看得到額度區塊、看不到金鑰區塊;②填超過上限存不進去並說明;③清空欄位顯示灰字繼承值;④聊到 80% 以上三支功能頁都出黃條;⑤新建一個租戶,查 system_configs 沒有它的 AI_QUOTA 列,但畫面顯示系統預設
FR-122.14 用量頁 ①BE:彙總 API(期間、分組、金鑰來源、租戶篩選限平台管理員;使用者分組補暱稱)②FE:「AI 用量」頁,依 D-g 與呼叫紀錄合併成兩分頁;匯出 .12 ①本月依使用者分組,各列加總=頂部總額;②租戶管理員看不到其他租戶、也看不到租戶篩選;③點「明細」切到呼叫明細並只剩該人該期間;④金鑰來源切「全部」數字變大(含客戶自帶鑰)
§16

design.md 改寫草稿

以下是裁完後要落進 design.md 的文字,本稿不改 design.md 本體,由下一棒照抄並依最終裁示調整。

§3.2 成本(第三階段)— 取代原表

題目 定案 理由 被排除方案
計量單位 美金,直接加總 ai_call_log.estimated_usd 每筆已有估價,不必再維護換算表;與業界閘道同單位 點數換算表(09-28 原裁):多一張要維護的表,且與實際花費脫鉤
額度層級 每租戶一個+每使用者一個(全租戶同數),任一超就擋;各為「金額+週期(日/月,預設日)」 與 LiteLLM、Bifrost 同做法;一個人用光全租戶額度的情況由每人額度擋 只有租戶層:一人可吃光全租戶;逐人設:管理負擔大
週期邊界 日曆日、伺服器本地時區(TZ)零點重算;月=日曆月 「明天 00:00 恢復」一句話講得清楚 滾動 24 小時:使用者看不懂何時恢復
算法 每次呼叫前 SUM 本期原廠鑰花費;並發時超一點點接受 已有索引、一天幾百筆等級,查詢 0.1 毫秒;免處理計數器歸零與對帳 Redis 計數器+對帳:多三個出錯點。反悔條件:日量上萬或 SaaS 共用 DB 時在 IQuotaCounter 實作層換,介面不動
每分鐘次數 閘道統一管(Redis 固定視窗),每人一個數、每租戶可設;分類一份=1 次、儀表板生成一次=1 次 原本兩支功能各算、分類沒管 維持各功能自己算:分類仍無上限
設定位置 system_configs 群組 AI_QUOTA;ROOT 那筆=系統預設;租戶管理員在上限內設自己租戶 每租戶可不同、沿用既有租戶→ROOT 讀法 放進 AI_PROVIDER_CONFIG 群組:該群組鎖平台管理員
原廠鑰定位 試用額度:有額度,用完引導客戶改自帶金鑰 不必先做簽發站「加購額度」 原廠鑰無上限:燒錢無底
硬上限到了 只擋 AI 呼叫,功能本身與歷史結果照常 既有儀表板與分類結果仍可看 整個模組停用

共同前提:第一版設定存 system_configs,license limits 預留(欄位名 ai_quota 已對齊,日後 SaaS 當上限的上限);只計原廠鑰(key_source 為 root/env,依 D-j)用量,客戶自帶金鑰不計也不擋;80% 告警只在畫面;分類批次送出前用歷史平均預估、明顯不夠整批先擋。

§6.3 第三階段:成本 — 取代原表

子需求 範圍
FR-122.12 額度與次數 閘道 IQuotaCounter 改收呼叫脈絡與金鑰來源、移到解析金鑰之後;新增 IRateLimiter;主專案加總式額度實作與 AI_QUOTA 讀取;兩支 AI 套件的次數限制退場;分類送出前預估
FR-122.13 設定頁 AI 服務設定頁加額度區塊(租戶管理員可設、平台管理員設上限);「我目前的額度」API;三支 AI 功能頁 80% 黃條
FR-122.14 用量頁 依 ai_call_log 彙總的「AI 用量」頁,與呼叫紀錄頁合併兩分頁

§7.3 第三階段驗收 — 取代原表

# 動作 預期
1 把自己的每人額度設很低,用小幫手聊到 80% 小幫手視窗出現黃條
2 繼續聊到用滿 回「今天額度已用完,明天 00:00 恢復」;儀表板與分類也被擋;既有儀表板與分類結果照常看得到
3 同租戶改填自己的金鑰再聊 不被擋,用量頁「原廠鑰」數字不增加
4 每分鐘次數設 2,連送 3 句 第 3 句回「請 N 秒後再試」
5 送一批明顯超過剩餘額度的分類 送出前被擋並說明可送幾份
6 用量頁選本月、依使用者 各列加總等於頂部總額,點明細只剩那個人的紀錄