FR-122.12-D · CM-2339 · 討論稿 · 2026-09-29
第一階段已把每次 AI 呼叫花多少美金記進 ai_call_log。這一稿講怎麼「管錢」:租戶與使用者各一個美金額度(日或月)、每分鐘次數改由閘道統一管、一頁看時間區間內的花費。全部已裁:9 項照寫,D-a、D-f~D-j 決策者 2026-09-29 裁定全照稿內建議。定案已落 design.md §3.2、§5.18、§6.3、§7.3。
開啟 mockup:設定區塊/AI 用量頁/額度用完時的畫面(深色主題、配色照 FE design tokens,三個分頁切換)
一句話:用原廠金鑰打 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 預設數字(其他待裁項都有建議,照建議即可開工)。
額度單位=美金,推翻 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);花費頁獨立一頁,系統管理員看全部、租戶管理員看自己。
| 方案 | 額度怎麼定 | 分幾層 | 超了怎樣 | 我們能不能直接用 |
|---|---|---|---|---|
| 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
以下每一列都已開檔核對。
| 元件 | 現況 | 本案動作 |
|---|---|---|
閘道的額度檢查點 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 毫秒。
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
resolve_retention()(租戶→ROOT→預設),用 read_tenant_config_value。-- 租戶層(使用者層多一個 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 呼叫本身要數秒,多這一句看不出來。date_trunc('day', now()):那是資料庫連線的時區,與應用容器的 TZ 不一定一樣,兩邊差 8 小時的症狀是「每天早上 8 點才歸零」,而且不會報錯。read_tenant_config_value 同一個樣板),因為分類的背景工作沒有請求脈絡。AI_CALL_LOG_RETENTION_DAYS 若被設成 31 天以下,月初的紀錄會先被清掉,月額度就少算。設定頁要擋「週期選月、保留天數 < 31」。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 功能都經主專案接線),風險低。
%%{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% 告警)
| 功能 | 現在 | 之後 |
|---|---|---|
| 小幫手 | 套件內每人 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)退場後變成沒人用的預設值,留在套件裡不影響行為;下次動這兩支套件時順手刪掉。
三支功能收到閘道擋下時,本來就各有「被擋」的呈現通道,不必新造:
| 功能 | 現有被擋通道(已開檔核對) | 額度用完 | 次數超過 |
|---|---|---|---|
| 小幫手 | 回 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 就原樣回」,所以套件不必改就能講清楚是哪一層、何時恢復。
| 項目 | 內容 |
|---|---|
| 篩選 | 期間(本日/本週/本月/自訂)、分組(租戶/使用者/功能/模型/日期)、金鑰來源(原廠鑰/全部,預設原廠鑰)、租戶(只有平台管理員看得到這個篩選) |
| 頂部三格 | 花費(美金)、呼叫次數、被擋下次數(額度+次數) |
| 表格欄 | 分組名稱、呼叫次數、輸入 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 合成一列「動態儀表板」,可展開看兩段。見 mockup 第①頁。重點:
AiServiceConfigForm.vue,253 行,加一塊不會超過 800 行上限;建議新區塊拆成獨立子元件)。AI_SERVICE_CONFIG_PLATFORM_ADMIN_ONLY,路由、選單、頁面三處同引)。額度區塊要讓租戶管理員進得來、但金鑰區塊仍只有平台管理員看得到——路由與選單改成「有 storage-config.read 就進得來」,頁內金鑰區塊照舊用那個旗標藏起來。InputNumber(兩位小數)+ Dropdown(日/月)+ ProgressBar;色彩用 design tokens,80% 以上用 --color-orange、滿了用 --color-red。以下各項決策者已裁定照「我的建議」走,保留原文供查理由。原列的 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 額度用完回什麼狀態碼
blocked=quota 與說明句);只有「分類送出前預估不夠」這支同步 API 回錯誤,用 412 前置條件不滿足+新錯誤碼 GRC_412xxx(照專案命名規則)。jedi_common 也沒有對應類別,要新造;而小幫手與儀表板的被擋本來就不是 HTTP 錯誤,改成 402 等於改兩支套件的回應契約。我的建議:A。
D-g 用量頁要不要跟現有「AI 呼叫紀錄」頁合併
我的建議:A。兩者同一顆能力點、同一張表,讀者要的是「先看總數、再點進去看是哪幾筆」。
D-h 租戶管理員能不能把額度調高過系統預設(新增)
租戶管理員自己設額度,但花的是原廠的錢。不設上限的話,租戶管理員填 1,000 美金/日就等於沒有額度。
AI_QUOTA/CEILING,只有平台管理員能改)。日後 SaaS 的 license 額度接在同一個位置。我的建議:A。
D-i 子租戶怎麼算(新增)
count_descendants 那類查法),加總查詢變重,且子租戶管理員看不到兄弟租戶花了多少,無從理解為什麼被擋。我的建議:A;若日後要「整個客戶一個總額度」,在 SaaS 的 license 額度那層做。
D-j 金鑰來源是「環境變數」的算不算原廠鑰(新增)
金鑰解析有三層:租戶自己的、ROOT 的(root)、環境變數的(env)。環境變數那把也是平台方的鑰,只是沒填在畫面上。
root 與 env 都計入額度;只有 tenant(客戶自己填的)不計。root:部署時用環境變數放原廠鑰的機器就完全沒有額度。我的建議:A。
| 子需求 | 範圍 | 依賴 | 驗收條件 |
|---|---|---|---|
| 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 | ①本月依使用者分組,各列加總=頂部總額;②租戶管理員看不到其他租戶、也看不到租戶篩選;③點「明細」切到呼叫明細並只剩該人該期間;④金鑰來源切「全部」數字變大(含客戶自帶鑰) |
以下是裁完後要落進 design.md 的文字,本稿不改 design.md 本體,由下一棒照抄並依最終裁示調整。
| 題目 | 定案 | 理由 | 被排除方案 |
|---|---|---|---|
| 計量單位 | 美金,直接加總 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% 告警只在畫面;分類批次送出前用歷史平均預估、明顯不夠整批先擋。
| 子需求 | 範圍 |
|---|---|
| FR-122.12 額度與次數 | 閘道 IQuotaCounter 改收呼叫脈絡與金鑰來源、移到解析金鑰之後;新增 IRateLimiter;主專案加總式額度實作與 AI_QUOTA 讀取;兩支 AI 套件的次數限制退場;分類送出前預估 |
| FR-122.13 設定頁 | AI 服務設定頁加額度區塊(租戶管理員可設、平台管理員設上限);「我目前的額度」API;三支 AI 功能頁 80% 黃條 |
| FR-122.14 用量頁 | 依 ai_call_log 彙總的「AI 用量」頁,與呼叫紀錄頁合併兩分頁 |
| # | 動作 | 預期 |
|---|---|---|
| 1 | 把自己的每人額度設很低,用小幫手聊到 80% | 小幫手視窗出現黃條 |
| 2 | 繼續聊到用滿 | 回「今天額度已用完,明天 00:00 恢復」;儀表板與分類也被擋;既有儀表板與分類結果照常看得到 |
| 3 | 同租戶改填自己的金鑰再聊 | 不被擋,用量頁「原廠鑰」數字不增加 |
| 4 | 每分鐘次數設 2,連送 3 句 | 第 3 句回「請 N 秒後再試」 |
| 5 | 送一批明顯超過剩餘額度的分類 | 送出前被擋並說明可送幾份 |
| 6 | 用量頁選本月、依使用者 | 各列加總等於頂部總額,點明細只剩那個人的紀錄 |