zen: display quota usage breakdown

This commit is contained in:
Frank
2026-08-25 05:06:33 -04:00
parent 1e86be2bc5
commit afd6f3b064
24 changed files with 743 additions and 48 deletions
+9 -1
View File
@@ -661,10 +661,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "بضع ثوان",
"workspace.lite.subscription.message": "أنت مشترك في OpenCode Go.",
"workspace.lite.subscription.manage": "إدارة الاشتراك",
"workspace.lite.subscription.rollingUsage": "الاستخدام المتجدد",
"workspace.lite.subscription.rollingUsage": "الاستخدام خلال 5 ساعات",
"workspace.lite.subscription.rollingQuota": "الحصة خلال 5 ساعات",
"workspace.lite.subscription.weeklyUsage": "الاستخدام الأسبوعي",
"workspace.lite.subscription.weeklyQuota": "الحصة الأسبوعية",
"workspace.lite.subscription.monthlyUsage": "الاستخدام الشهري",
"workspace.lite.subscription.monthlyQuota": "الحصة الشهرية",
"workspace.lite.subscription.resetsIn": "إعادة تعيين في",
"workspace.lite.subscription.showDetails": "إظهار التفاصيل",
"workspace.lite.subscription.hideDetails": "إخفاء التفاصيل",
"workspace.lite.subscription.model": "النموذج",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "الإجمالي",
"workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
"workspace.lite.subscription.selectProvider":
'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.',
+9 -1
View File
@@ -673,10 +673,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "alguns segundos",
"workspace.lite.subscription.message": "Você assina o OpenCode Go.",
"workspace.lite.subscription.manage": "Gerenciar Assinatura",
"workspace.lite.subscription.rollingUsage": "Uso Contínuo",
"workspace.lite.subscription.rollingUsage": "Uso de 5 horas",
"workspace.lite.subscription.rollingQuota": "Cota de 5 horas",
"workspace.lite.subscription.weeklyUsage": "Uso Semanal",
"workspace.lite.subscription.weeklyQuota": "Cota Semanal",
"workspace.lite.subscription.monthlyUsage": "Uso Mensal",
"workspace.lite.subscription.monthlyQuota": "Cota Mensal",
"workspace.lite.subscription.resetsIn": "Reinicia em",
"workspace.lite.subscription.showDetails": "Mostrar detalhes",
"workspace.lite.subscription.hideDetails": "Ocultar detalhes",
"workspace.lite.subscription.model": "Modelo",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Total",
"workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso",
"workspace.lite.subscription.selectProvider":
'Selecione "OpenCode Go" como provedor na sua configuração do opencode para usar os modelos Go.',
+9 -1
View File
@@ -669,10 +669,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "et par sekunder",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.",
"workspace.lite.subscription.manage": "Administrer abonnement",
"workspace.lite.subscription.rollingUsage": "Løbende forbrug",
"workspace.lite.subscription.rollingUsage": "5-timers forbrug",
"workspace.lite.subscription.rollingQuota": "5-timers kvote",
"workspace.lite.subscription.weeklyUsage": "Ugentligt forbrug",
"workspace.lite.subscription.weeklyQuota": "Ugentlig kvote",
"workspace.lite.subscription.monthlyUsage": "Månedligt forbrug",
"workspace.lite.subscription.monthlyQuota": "Månedlig kvote",
"workspace.lite.subscription.resetsIn": "Nulstiller i",
"workspace.lite.subscription.showDetails": "Vis detaljer",
"workspace.lite.subscription.hideDetails": "Skjul detaljer",
"workspace.lite.subscription.model": "Model",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "I alt",
"workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne",
"workspace.lite.subscription.selectProvider":
'Vælg "OpenCode Go" som udbyder i din opencode-konfiguration for at bruge Go-modeller.',
+9 -1
View File
@@ -671,10 +671,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "einige Sekunden",
"workspace.lite.subscription.message": "Du hast OpenCode Go abonniert.",
"workspace.lite.subscription.manage": "Abo verwalten",
"workspace.lite.subscription.rollingUsage": "Fortlaufende Nutzung",
"workspace.lite.subscription.rollingUsage": "5-Stunden-Nutzung",
"workspace.lite.subscription.rollingQuota": "5-Stunden-Kontingent",
"workspace.lite.subscription.weeklyUsage": "Wöchentliche Nutzung",
"workspace.lite.subscription.weeklyQuota": "Wöchentliches Kontingent",
"workspace.lite.subscription.monthlyUsage": "Monatliche Nutzung",
"workspace.lite.subscription.monthlyQuota": "Monatliches Kontingent",
"workspace.lite.subscription.resetsIn": "Setzt zurück in",
"workspace.lite.subscription.showDetails": "Details anzeigen",
"workspace.lite.subscription.hideDetails": "Details ausblenden",
"workspace.lite.subscription.model": "Modell",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Gesamt",
"workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind",
"workspace.lite.subscription.selectProvider":
'Wähle "OpenCode Go" als Anbieter in deiner opencode-Konfiguration, um Go-Modelle zu verwenden.',
+9 -1
View File
@@ -669,10 +669,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "a few seconds",
"workspace.lite.subscription.message": "You are subscribed to OpenCode Go.",
"workspace.lite.subscription.manage": "Manage Subscription",
"workspace.lite.subscription.rollingUsage": "Rolling Usage",
"workspace.lite.subscription.rollingUsage": "5-hour Usage",
"workspace.lite.subscription.rollingQuota": "5-hour Quota",
"workspace.lite.subscription.weeklyUsage": "Weekly Usage",
"workspace.lite.subscription.weeklyQuota": "Weekly Quota",
"workspace.lite.subscription.monthlyUsage": "Monthly Usage",
"workspace.lite.subscription.monthlyQuota": "Monthly Quota",
"workspace.lite.subscription.resetsIn": "Resets in",
"workspace.lite.subscription.showDetails": "Show details",
"workspace.lite.subscription.hideDetails": "Hide details",
"workspace.lite.subscription.model": "Model",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Total",
"workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits",
"workspace.lite.subscription.selectProvider":
'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.',
+9 -1
View File
@@ -674,10 +674,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "unos pocos segundos",
"workspace.lite.subscription.message": "Estás suscrito a OpenCode Go.",
"workspace.lite.subscription.manage": "Gestionar Suscripción",
"workspace.lite.subscription.rollingUsage": "Uso Continuo",
"workspace.lite.subscription.rollingUsage": "Uso de 5 horas",
"workspace.lite.subscription.rollingQuota": "Cuota de 5 horas",
"workspace.lite.subscription.weeklyUsage": "Uso Semanal",
"workspace.lite.subscription.weeklyQuota": "Cuota Semanal",
"workspace.lite.subscription.monthlyUsage": "Uso Mensual",
"workspace.lite.subscription.monthlyQuota": "Cuota Mensual",
"workspace.lite.subscription.resetsIn": "Se reinicia en",
"workspace.lite.subscription.showDetails": "Mostrar detalles",
"workspace.lite.subscription.hideDetails": "Ocultar detalles",
"workspace.lite.subscription.model": "Modelo",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Total",
"workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso",
"workspace.lite.subscription.selectProvider":
'Selecciona "OpenCode Go" como proveedor en tu configuración de opencode para usar los modelos Go.',
+9 -1
View File
@@ -679,10 +679,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "quelques secondes",
"workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Go.",
"workspace.lite.subscription.manage": "Gérer l'abonnement",
"workspace.lite.subscription.rollingUsage": "Utilisation glissante",
"workspace.lite.subscription.rollingUsage": "Utilisation sur 5 heures",
"workspace.lite.subscription.rollingQuota": "Quota sur 5 heures",
"workspace.lite.subscription.weeklyUsage": "Utilisation hebdomadaire",
"workspace.lite.subscription.weeklyQuota": "Quota hebdomadaire",
"workspace.lite.subscription.monthlyUsage": "Utilisation mensuelle",
"workspace.lite.subscription.monthlyQuota": "Quota mensuel",
"workspace.lite.subscription.resetsIn": "Réinitialisation dans",
"workspace.lite.subscription.showDetails": "Afficher les détails",
"workspace.lite.subscription.hideDetails": "Masquer les détails",
"workspace.lite.subscription.model": "Modèle",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Total",
"workspace.lite.subscription.useBalance":
"Utilisez votre solde disponible après avoir atteint les limites d'utilisation",
"workspace.lite.subscription.selectProvider":
+9 -1
View File
@@ -672,10 +672,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "pochi secondi",
"workspace.lite.subscription.message": "Sei abbonato a OpenCode Go.",
"workspace.lite.subscription.manage": "Gestisci Abbonamento",
"workspace.lite.subscription.rollingUsage": "Utilizzo Continuativo",
"workspace.lite.subscription.rollingUsage": "Utilizzo su 5 ore",
"workspace.lite.subscription.rollingQuota": "Quota su 5 ore",
"workspace.lite.subscription.weeklyUsage": "Utilizzo Settimanale",
"workspace.lite.subscription.weeklyQuota": "Quota Settimanale",
"workspace.lite.subscription.monthlyUsage": "Utilizzo Mensile",
"workspace.lite.subscription.monthlyQuota": "Quota Mensile",
"workspace.lite.subscription.resetsIn": "Si resetta tra",
"workspace.lite.subscription.showDetails": "Mostra dettagli",
"workspace.lite.subscription.hideDetails": "Nascondi dettagli",
"workspace.lite.subscription.model": "Modello",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Totale",
"workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo",
"workspace.lite.subscription.selectProvider":
'Seleziona "OpenCode Go" come provider nella tua configurazione opencode per utilizzare i modelli Go.',
+9 -1
View File
@@ -670,10 +670,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "数秒",
"workspace.lite.subscription.message": "あなたは OpenCode Go を購読しています。",
"workspace.lite.subscription.manage": "サブスクリプションの管理",
"workspace.lite.subscription.rollingUsage": "ローリング利用量",
"workspace.lite.subscription.rollingUsage": "5時間利用量",
"workspace.lite.subscription.rollingQuota": "5時間上限",
"workspace.lite.subscription.weeklyUsage": "週間利用量",
"workspace.lite.subscription.weeklyQuota": "週間上限",
"workspace.lite.subscription.monthlyUsage": "月間利用量",
"workspace.lite.subscription.monthlyQuota": "月間上限",
"workspace.lite.subscription.resetsIn": "リセットまで",
"workspace.lite.subscription.showDetails": "詳細を表示",
"workspace.lite.subscription.hideDetails": "詳細を非表示",
"workspace.lite.subscription.model": "モデル",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "合計",
"workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する",
"workspace.lite.subscription.selectProvider":
"Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。",
+9 -1
View File
@@ -661,10 +661,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "몇 초",
"workspace.lite.subscription.message": "현재 OpenCode Go를 구독 중입니다.",
"workspace.lite.subscription.manage": "구독 관리",
"workspace.lite.subscription.rollingUsage": "롤링 사용량",
"workspace.lite.subscription.rollingUsage": "5시간 사용량",
"workspace.lite.subscription.rollingQuota": "5시간 할당량",
"workspace.lite.subscription.weeklyUsage": "주간 사용량",
"workspace.lite.subscription.weeklyQuota": "주간 할당량",
"workspace.lite.subscription.monthlyUsage": "월간 사용량",
"workspace.lite.subscription.monthlyQuota": "월간 할당량",
"workspace.lite.subscription.resetsIn": "초기화까지 남은 시간:",
"workspace.lite.subscription.showDetails": "상세 정보 보기",
"workspace.lite.subscription.hideDetails": "상세 정보 숨기기",
"workspace.lite.subscription.model": "모델",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "합계",
"workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용",
"workspace.lite.subscription.selectProvider":
'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.',
+9 -1
View File
@@ -670,10 +670,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "noen få sekunder",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.",
"workspace.lite.subscription.manage": "Administrer abonnement",
"workspace.lite.subscription.rollingUsage": "Løpende bruk",
"workspace.lite.subscription.rollingUsage": "5-timers bruk",
"workspace.lite.subscription.rollingQuota": "5-timers kvote",
"workspace.lite.subscription.weeklyUsage": "Ukentlig bruk",
"workspace.lite.subscription.weeklyQuota": "Ukentlig kvote",
"workspace.lite.subscription.monthlyUsage": "Månedlig bruk",
"workspace.lite.subscription.monthlyQuota": "Månedlig kvote",
"workspace.lite.subscription.resetsIn": "Nullstilles om",
"workspace.lite.subscription.showDetails": "Vis detaljer",
"workspace.lite.subscription.hideDetails": "Skjul detaljer",
"workspace.lite.subscription.model": "Modell",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Totalt",
"workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene",
"workspace.lite.subscription.selectProvider":
'Velg "OpenCode Go" som leverandør i opencode-konfigurasjonen din for å bruke Go-modeller.',
+9 -1
View File
@@ -671,10 +671,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "kilka sekund",
"workspace.lite.subscription.message": "Subskrybujesz OpenCode Go.",
"workspace.lite.subscription.manage": "Zarządzaj subskrypcją",
"workspace.lite.subscription.rollingUsage": "Użycie kroczące",
"workspace.lite.subscription.rollingUsage": "Użycie w ciągu 5 godzin",
"workspace.lite.subscription.rollingQuota": "Limit 5-godzinny",
"workspace.lite.subscription.weeklyUsage": "Użycie tygodniowe",
"workspace.lite.subscription.weeklyQuota": "Limit tygodniowy",
"workspace.lite.subscription.monthlyUsage": "Użycie miesięczne",
"workspace.lite.subscription.monthlyQuota": "Limit miesięczny",
"workspace.lite.subscription.resetsIn": "Resetuje się za",
"workspace.lite.subscription.showDetails": "Pokaż szczegóły",
"workspace.lite.subscription.hideDetails": "Ukryj szczegóły",
"workspace.lite.subscription.model": "Model",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Łącznie",
"workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia",
"workspace.lite.subscription.selectProvider":
'Wybierz "OpenCode Go" jako dostawcę w konfiguracji opencode, aby używać modeli Go.',
+9 -1
View File
@@ -678,10 +678,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "несколько секунд",
"workspace.lite.subscription.message": "Вы подписаны на OpenCode Go.",
"workspace.lite.subscription.manage": "Управление подпиской",
"workspace.lite.subscription.rollingUsage": "Скользящее использование",
"workspace.lite.subscription.rollingUsage": "Использование за 5 часов",
"workspace.lite.subscription.rollingQuota": "Квота на 5 часов",
"workspace.lite.subscription.weeklyUsage": "Недельное использование",
"workspace.lite.subscription.weeklyQuota": "Недельная квота",
"workspace.lite.subscription.monthlyUsage": "Ежемесячное использование",
"workspace.lite.subscription.monthlyQuota": "Ежемесячная квота",
"workspace.lite.subscription.resetsIn": "Сброс через",
"workspace.lite.subscription.showDetails": "Показать подробности",
"workspace.lite.subscription.hideDetails": "Скрыть подробности",
"workspace.lite.subscription.model": "Модель",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Итого",
"workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов",
"workspace.lite.subscription.selectProvider":
'Выберите "OpenCode Go" в качестве провайдера в настройках opencode для использования моделей Go.',
+9 -1
View File
@@ -667,10 +667,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "ไม่กี่วินาที",
"workspace.lite.subscription.message": "คุณได้สมัครสมาชิก OpenCode Go แล้ว",
"workspace.lite.subscription.manage": "จัดการการสมัครสมาชิก",
"workspace.lite.subscription.rollingUsage": "การใช้งานแบบหมุนเวียน",
"workspace.lite.subscription.rollingUsage": "การใช้งานใน 5 ชั่วโมง",
"workspace.lite.subscription.rollingQuota": "โควตา 5 ชั่วโมง",
"workspace.lite.subscription.weeklyUsage": "การใช้งานรายสัปดาห์",
"workspace.lite.subscription.weeklyQuota": "โควตารายสัปดาห์",
"workspace.lite.subscription.monthlyUsage": "การใช้งานรายเดือน",
"workspace.lite.subscription.monthlyQuota": "โควตารายเดือน",
"workspace.lite.subscription.resetsIn": "รีเซ็ตใน",
"workspace.lite.subscription.showDetails": "แสดงรายละเอียด",
"workspace.lite.subscription.hideDetails": "ซ่อนรายละเอียด",
"workspace.lite.subscription.model": "โมเดล",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "รวม",
"workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน",
"workspace.lite.subscription.selectProvider":
'เลือก "OpenCode Go" เป็นผู้ให้บริการในการตั้งค่า opencode ของคุณเพื่อใช้โมเดล Go',
+9 -1
View File
@@ -673,10 +673,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "birkaç saniye",
"workspace.lite.subscription.message": "OpenCode Go abonesisiniz.",
"workspace.lite.subscription.manage": "Aboneliği Yönet",
"workspace.lite.subscription.rollingUsage": "Devam Eden Kullanım",
"workspace.lite.subscription.rollingUsage": "5 Saatlik Kullanım",
"workspace.lite.subscription.rollingQuota": "5 Saatlik Kota",
"workspace.lite.subscription.weeklyUsage": "Haftalık Kullanım",
"workspace.lite.subscription.weeklyQuota": "Haftalık Kota",
"workspace.lite.subscription.monthlyUsage": "Aylık Kullanım",
"workspace.lite.subscription.monthlyQuota": "Aylık Kota",
"workspace.lite.subscription.resetsIn": "Sıfırlama süresi",
"workspace.lite.subscription.showDetails": "Ayrıntıları göster",
"workspace.lite.subscription.hideDetails": "Ayrıntıları gizle",
"workspace.lite.subscription.model": "Model",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Toplam",
"workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın",
"workspace.lite.subscription.selectProvider":
'Go modellerini kullanmak için opencode yapılandırmanızda "OpenCode Go"\'yu sağlayıcı olarak seçin.',
+9 -1
View File
@@ -669,10 +669,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "кілька секунд",
"workspace.lite.subscription.message": "Ви підписані на OpenCode Go.",
"workspace.lite.subscription.manage": "Керувати підпискою",
"workspace.lite.subscription.rollingUsage": "Ковзне використання",
"workspace.lite.subscription.rollingUsage": "Використання за 5 годин",
"workspace.lite.subscription.rollingQuota": "Квота на 5 годин",
"workspace.lite.subscription.weeklyUsage": "Тижневе використання",
"workspace.lite.subscription.weeklyQuota": "Тижнева квота",
"workspace.lite.subscription.monthlyUsage": "Місячне використання",
"workspace.lite.subscription.monthlyQuota": "Місячна квота",
"workspace.lite.subscription.resetsIn": "Скидається через",
"workspace.lite.subscription.showDetails": "Показати подробиці",
"workspace.lite.subscription.hideDetails": "Приховати подробиці",
"workspace.lite.subscription.model": "Модель",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Усього",
"workspace.lite.subscription.useBalance": "Використовуйте доступний баланс після досягнення лімітів",
"workspace.lite.subscription.selectProvider": 'Виберіть "OpenCode Go" як провайдера в конфігурації opencode.',
"workspace.lite.providers.title": "Провайдери",
+9 -1
View File
@@ -642,10 +642,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "几秒钟",
"workspace.lite.subscription.message": "您已订阅 OpenCode Go。",
"workspace.lite.subscription.manage": "管理订阅",
"workspace.lite.subscription.rollingUsage": "滚动用量",
"workspace.lite.subscription.rollingUsage": "5 小时用量",
"workspace.lite.subscription.rollingQuota": "5 小时配额",
"workspace.lite.subscription.weeklyUsage": "每周用量",
"workspace.lite.subscription.weeklyQuota": "每周配额",
"workspace.lite.subscription.monthlyUsage": "每月用量",
"workspace.lite.subscription.monthlyQuota": "每月配额",
"workspace.lite.subscription.resetsIn": "重置于",
"workspace.lite.subscription.showDetails": "显示详情",
"workspace.lite.subscription.hideDetails": "隐藏详情",
"workspace.lite.subscription.model": "模型",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "总计",
"workspace.lite.subscription.useBalance": "达到使用限额后使用您的可用余额",
"workspace.lite.subscription.selectProvider":
"在你的 opencode 配置中选择「OpenCode Go」作为提供商,即可使用 Go 模型。",
+9 -1
View File
@@ -642,10 +642,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "幾秒",
"workspace.lite.subscription.message": "您已訂閱 OpenCode Go。",
"workspace.lite.subscription.manage": "管理訂閱",
"workspace.lite.subscription.rollingUsage": "滾動使用量",
"workspace.lite.subscription.rollingUsage": "5 小時使用量",
"workspace.lite.subscription.rollingQuota": "5 小時配額",
"workspace.lite.subscription.weeklyUsage": "每週使用量",
"workspace.lite.subscription.weeklyQuota": "每週配額",
"workspace.lite.subscription.monthlyUsage": "每月使用量",
"workspace.lite.subscription.monthlyQuota": "每月配額",
"workspace.lite.subscription.resetsIn": "重置時間:",
"workspace.lite.subscription.showDetails": "顯示詳情",
"workspace.lite.subscription.hideDetails": "隱藏詳情",
"workspace.lite.subscription.model": "模型",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "總計",
"workspace.lite.subscription.useBalance": "達到使用限制後使用您的可用餘額",
"workspace.lite.subscription.selectProvider":
"在您的 opencode 設定中選擇「OpenCode Go」作為提供商,即可使用 Go 模型。",
@@ -0,0 +1,64 @@
export type LiteUsageBreakdownSource = {
model: string
name: string
cost: number
quotaCost: number
multiplier?: number
estimated: boolean
}
export type LiteUsageBreakdownItem = {
model: string
name: string
cost?: number
multiplier?: number
quotaCost: number
contributionPercent: number
estimated: boolean
}
export function buildLiteUsageBreakdown(input: {
usage: number
limit: number
sources: LiteUsageBreakdownSource[]
}) {
const rows: LiteUsageBreakdownItem[] = input.sources
.filter((item) => item.cost !== 0 || item.quotaCost !== 0)
.sort((a, b) => b.quotaCost - a.quotaCost)
.map((item) => ({
...item,
contributionPercent: 0,
}))
const usagePercent = getUsagePercent(input.usage, input.limit)
const target = Math.max(0, Math.round(usagePercent * 10))
const totalQuota = rows.reduce((total, item) => total + Math.max(0, item.quotaCost), 0)
const units = rows.map((item) => {
const exact = totalQuota === 0 ? 0 : (Math.max(0, item.quotaCost) / totalQuota) * target
const value = Math.floor(exact)
return { item, exact, value }
})
const remaining = target - units.reduce((total, item) => total + item.value, 0)
const ranked = units.toSorted((a, b) => b.exact - b.value - (a.exact - a.value))
Array.from({ length: ranked.length === 0 ? 0 : remaining }).forEach((_, index) => {
ranked[index % ranked.length].value += 1
})
units.forEach((unit) => (unit.item.contributionPercent = unit.value / 10))
return {
usage: input.usage,
limit: input.limit,
usagePercent,
rows,
}
}
export function getModelQuotaLimit(limit: number, multiplier?: number) {
if (multiplier === undefined || multiplier <= 0) return
return limit / multiplier
}
export function getUsagePercent(amount: number, limit: number) {
if (limit === 0) return 0
return Math.round((amount / limit) * 1000) / 10
}
@@ -19,6 +19,7 @@
[data-slot="usage-item"] {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
@@ -60,6 +61,119 @@
color: var(--color-text-muted);
}
[data-slot="usage-details"] {
margin-top: var(--space-1);
}
[data-slot="usage-details-trigger"] {
display: inline-flex;
align-items: center;
gap: var(--space-2);
width: fit-content;
padding: 0;
border: 0;
background-color: transparent;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
cursor: pointer;
&:hover:not(:disabled) {
background-color: transparent;
border-color: transparent;
}
svg {
transition: transform 0.2s ease;
}
&[aria-expanded="true"] svg {
transform: rotate(180deg);
}
[data-slot="hide-details"] {
display: none;
}
&[aria-expanded="true"] [data-slot="show-details"] {
display: none;
}
&[aria-expanded="true"] [data-slot="hide-details"] {
display: inline;
}
}
[data-slot="usage-details-content"] {
width: 100%;
margin-top: var(--space-3);
}
[data-slot="usage-details-loading"] {
width: 100%;
margin-top: var(--space-3);
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
[data-slot="usage-details-empty"] {
margin: 0;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
[data-slot="usage-details-table"] {
overflow-x: auto;
border: 1px solid var(--color-border-muted);
border-radius: var(--border-radius-sm);
table {
width: 100%;
min-width: 28rem;
table-layout: fixed;
border-collapse: collapse;
font-size: var(--font-size-sm);
white-space: nowrap;
}
th,
td {
width: 25%;
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--color-border-muted);
text-align: end;
font-variant-numeric: tabular-nums;
}
th {
color: var(--color-text-muted);
font-size: var(--font-size-xs);
font-weight: 500;
text-transform: uppercase;
}
th:first-child,
td:first-child {
text-align: start;
}
td:first-child {
max-width: 13rem;
overflow: hidden;
color: var(--color-text);
text-overflow: ellipsis;
}
tbody tr:last-child td {
border-bottom: 0;
}
[data-slot="usage-total"] td {
border-top: 1px solid var(--color-border);
color: var(--color-text);
font-weight: 600;
}
}
[data-slot="setting-row"] {
display: flex;
align-items: center;
@@ -1,15 +1,19 @@
import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router"
import { createStore } from "solid-js/store"
import { createMemo, For, Show } from "solid-js"
import { createMemo, createSignal, For, Show } from "solid-js"
import { Modal } from "~/component/modal"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { BillingTable, LiteTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { Database, eq, and, gte, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { BillingTable, LiteTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { Workspace } from "@opencode-ai/console-core/workspace.js"
import { Subscription } from "@opencode-ai/console-core/subscription.js"
import { LiteData } from "@opencode-ai/console-core/lite.js"
import { ZenData } from "@opencode-ai/console-core/model.js"
import { getMonthlyBounds, getWeekBounds } from "@opencode-ai/console-core/util/date.js"
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
import { withActor } from "~/context/auth.withActor"
import { queryBillingInfo } from "../../common"
import styles from "./lite-section.module.css"
@@ -21,7 +25,10 @@ import { createReferralFromCookie } from "~/lib/referral-invite"
import { getRequestEvent } from "solid-js/web"
import { countryFromRequest } from "~/lib/request-country"
import { IconAlipay, IconUpi } from "~/component/icon"
import { IconAlipay, IconChevron, IconUpi } from "~/component/icon"
import { buildLiteUsageBreakdown, getModelQuotaLimit, getUsagePercent } from "~/lib/lite-usage"
type LiteUsageWindow = "rolling" | "weekly" | "monthly"
export const queryLiteSubscription = query(async (workspaceID: string) => {
"use server"
@@ -51,6 +58,33 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
const limits = LiteData.getLimits()
const mine = row.userID === Actor.userID()
const now = new Date()
const rollingCutoff = new Date(now.getTime() - limits.rollingWindow * 3600 * 1000)
const week = getWeekBounds(now)
const month = getMonthlyBounds(now, row.timeCreated)
const rollingActive = !!row.timeRollingUpdated && row.timeRollingUpdated >= rollingCutoff
const weeklyActive = !!row.timeWeeklyUpdated && row.timeWeeklyUpdated >= week.start
const monthlyActive = !!row.timeMonthlyUpdated && row.timeMonthlyUpdated >= month.start
const rollingLimit = centsToMicroCents(limits.rollingLimit * 100)
const weeklyLimit = centsToMicroCents(limits.weeklyLimit * 100)
const monthlyLimit = centsToMicroCents(limits.monthlyLimit * 100)
const rollingUsage = Subscription.analyzeRollingUsage({
limit: limits.rollingLimit,
window: limits.rollingWindow,
usage: row.rollingUsage ?? 0,
timeUpdated: row.timeRollingUpdated ?? now,
})
const weeklyUsage = Subscription.analyzeWeeklyUsage({
limit: limits.weeklyLimit,
usage: row.weeklyUsage ?? 0,
timeUpdated: row.timeWeeklyUpdated ?? now,
})
const monthlyUsage = Subscription.analyzeMonthlyUsage({
limit: limits.monthlyLimit,
usage: row.monthlyUsage ?? 0,
timeUpdated: row.timeMonthlyUpdated ?? now,
timeSubscribed: row.timeCreated,
})
return {
mine,
@@ -58,27 +92,124 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
allowTraining: row.allowTraining ?? false,
region:
row.region ?? (await Workspace.setDefaultRegion({ country: countryFromRequest(getRequestEvent()?.request) })),
rollingUsage: Subscription.analyzeRollingUsage({
limit: limits.rollingLimit,
window: limits.rollingWindow,
usage: row.rollingUsage ?? 0,
timeUpdated: row.timeRollingUpdated ?? new Date(),
}),
weeklyUsage: Subscription.analyzeWeeklyUsage({
limit: limits.weeklyLimit,
usage: row.weeklyUsage ?? 0,
timeUpdated: row.timeWeeklyUpdated ?? new Date(),
}),
monthlyUsage: Subscription.analyzeMonthlyUsage({
limit: limits.monthlyLimit,
usage: row.monthlyUsage ?? 0,
timeUpdated: row.timeMonthlyUpdated ?? new Date(),
timeSubscribed: row.timeCreated,
}),
rollingUsage: {
...rollingUsage,
usage: rollingActive ? (row.rollingUsage ?? 0) : 0,
limit: rollingLimit,
usagePercent: getUsagePercent(rollingActive ? (row.rollingUsage ?? 0) : 0, rollingLimit),
},
weeklyUsage: {
...weeklyUsage,
usage: weeklyActive ? (row.weeklyUsage ?? 0) : 0,
limit: weeklyLimit,
usagePercent: getUsagePercent(weeklyActive ? (row.weeklyUsage ?? 0) : 0, weeklyLimit),
},
monthlyUsage: {
...monthlyUsage,
usage: monthlyActive ? (row.monthlyUsage ?? 0) : 0,
limit: monthlyLimit,
usagePercent: getUsagePercent(monthlyActive ? (row.monthlyUsage ?? 0) : 0, monthlyLimit),
},
}
}, workspaceID)
}, "lite.subscription.get")
export const queryLiteUsageDetails = query(async (workspaceID: string, window: LiteUsageWindow) => {
"use server"
return withActor(async () => {
if (window !== "rolling" && window !== "weekly" && window !== "monthly") return null
const row = await Database.use((tx) =>
tx
.select({
userID: LiteTable.userID,
rollingUsage: LiteTable.rollingUsage,
weeklyUsage: LiteTable.weeklyUsage,
monthlyUsage: LiteTable.monthlyUsage,
timeRollingUpdated: LiteTable.timeRollingUpdated,
timeWeeklyUpdated: LiteTable.timeWeeklyUpdated,
timeMonthlyUpdated: LiteTable.timeMonthlyUpdated,
timeCreated: LiteTable.timeCreated,
})
.from(LiteTable)
.where(and(eq(LiteTable.workspaceID, Actor.workspace()), isNull(LiteTable.timeDeleted)))
.then((result) => result[0]),
)
if (!row || row.userID !== Actor.userID()) return null
const limits = LiteData.getLimits()
const now = new Date()
const detail = (() => {
if (window === "rolling") {
const active = !!row.timeRollingUpdated && row.timeRollingUpdated >= new Date(now.getTime() - limits.rollingWindow * 3600 * 1000)
return {
start: active ? row.timeRollingUpdated! : now,
usage: active ? (row.rollingUsage ?? 0) : 0,
limit: centsToMicroCents(limits.rollingLimit * 100),
}
}
if (window === "weekly") {
const start = getWeekBounds(now).start
return {
start,
usage: row.timeWeeklyUpdated && row.timeWeeklyUpdated >= start ? (row.weeklyUsage ?? 0) : 0,
limit: centsToMicroCents(limits.weeklyLimit * 100),
}
}
const start = getMonthlyBounds(now, row.timeCreated).start
return {
start,
usage: row.timeMonthlyUpdated && row.timeMonthlyUpdated >= start ? (row.monthlyUsage ?? 0) : 0,
limit: centsToMicroCents(limits.monthlyLimit * 100),
}
})()
const modelData = Object.fromEntries(
Object.entries(ZenData.list("lite").models).map(([id, value]) => {
const models = Array.isArray(value) ? value : [value]
const multipliers = new Set(models.map((model) => model.costMultiplier))
return [id, { name: models[0].name, multiplier: multipliers.size === 1 ? models[0].costMultiplier : undefined }]
}),
)
const usageRows = await Database.use((tx) =>
tx
.select({
model: UsageTable.model,
multiplier: sql<string | null>`JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier'))`,
cost: sql<string>`SUM(${UsageTable.cost})`,
quotaCost: sql<string>`SUM(CASE WHEN JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier') IS NOT NULL THEN ROUND(${UsageTable.cost} * CAST(JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier')) AS DECIMAL(20, 8))) ELSE 0 END)`,
})
.from(UsageTable)
.innerJoin(KeyTable, and(eq(KeyTable.id, UsageTable.keyID), eq(KeyTable.workspaceID, UsageTable.workspaceID)))
.where(
and(
eq(UsageTable.workspaceID, Actor.workspace()),
eq(KeyTable.userID, row.userID),
gte(UsageTable.timeCreated, detail.start),
sql`JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')) = 'lite'`,
),
)
.groupBy(UsageTable.model, sql`JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier'))`),
)
return buildLiteUsageBreakdown({
usage: detail.usage,
limit: detail.limit,
sources: usageRows.map((usage) => {
const cost = Number(usage.cost)
const info = modelData[usage.model]
const multiplier = usage.multiplier === null ? info?.multiplier : Number(usage.multiplier)
return {
model: usage.model,
name: info?.name ?? usage.model,
cost,
quotaCost: usage.multiplier === null ? Math.round(cost * (multiplier ?? 1)) : Number(usage.quotaCost),
multiplier,
estimated: usage.multiplier === null,
}
}),
})
}, workspaceID)
}, "lite.subscription.usage")
type LiteSubscription = Awaited<ReturnType<typeof queryLiteSubscription>>
const createLiteCheckoutUrl = action(
@@ -174,7 +305,16 @@ const setGoAllowTraining = action(async (form: FormData) => {
)
}, "go.allowTraining.set")
function LiteUsageItem(props: { label: string; usage: { usagePercent: number; resetInSec: number } }) {
type LiteUsage = NonNullable<LiteSubscription>["rollingUsage"]
type LiteUsageDetailsData = NonNullable<Awaited<ReturnType<typeof queryLiteUsageDetails>>>
function LiteUsageItem(props: {
id: LiteUsageWindow
label: string
usage: LiteUsage
open: boolean
onToggle: () => void
}) {
const i18n = useI18n()
return (
@@ -183,17 +323,178 @@ function LiteUsageItem(props: { label: string; usage: { usagePercent: number; re
<span data-slot="usage-label">{props.label}</span>
<span data-slot="usage-value">{props.usage.usagePercent}%</span>
</div>
<div data-slot="progress">
<div data-slot="progress-bar" style={{ width: `${props.usage.usagePercent}%` }} />
<div
data-slot="progress"
role="progressbar"
aria-label={props.label}
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow={Math.min(100, props.usage.usagePercent)}
>
<div data-slot="progress-bar" style={{ width: `${Math.min(100, props.usage.usagePercent)}%` }} />
</div>
<span data-slot="reset-time">
{i18n.t("workspace.lite.subscription.resetsIn")}{" "}
{formatResetTime(props.usage.resetInSec, i18n, liteResetTimeKeys)}
</span>
<Show when={props.usage.usage > 0}>
<div data-slot="usage-details">
<button
type="button"
data-slot="usage-details-trigger"
aria-expanded={props.open}
aria-controls={`usage-details-${props.id}`}
onClick={props.onToggle}
>
<span data-slot="show-details">{i18n.t("workspace.lite.subscription.showDetails")}</span>
<span data-slot="hide-details">{i18n.t("workspace.lite.subscription.hideDetails")}</span>
<IconChevron />
</button>
</div>
</Show>
</div>
)
}
function LiteUsageDetails(props: { id: LiteUsageWindow; label: string; quotaLabel: string; usage: LiteUsageDetailsData }) {
const i18n = useI18n()
const language = useLanguage()
const money = (amount: number) =>
new Intl.NumberFormat(language.tag(language.locale()), {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 4,
}).format(amount / 100_000_000)
const totalPercentage = () =>
Number(props.usage.rows.reduce((total, row) => total + row.contributionPercent, 0).toFixed(1))
return (
<div id={`usage-details-${props.id}`} data-slot="usage-details-content" role="region" aria-label={props.label}>
<div data-slot="usage-details-table">
<table>
<thead>
<tr>
<th>{i18n.t("workspace.lite.subscription.model")}</th>
<th>{props.label}</th>
<th>{props.quotaLabel}</th>
<th>{i18n.t("workspace.lite.subscription.contribution")}</th>
</tr>
</thead>
<tbody>
<For each={props.usage.rows}>
{(row) => {
const quota = getModelQuotaLimit(props.usage.limit, row.multiplier)
return (
<tr>
<td>
<bdi dir="auto">{row.name}</bdi>
</td>
<td>{row.cost === undefined ? "-" : money(row.cost)}</td>
<td>{quota === undefined ? "-" : money(quota)}</td>
<td>{row.contributionPercent}%</td>
</tr>
)
}}
</For>
<tr data-slot="usage-total">
<td colSpan={3}>{i18n.t("workspace.lite.subscription.total")}</td>
<td>{totalPercentage()}%</td>
</tr>
</tbody>
</table>
</div>
</div>
)
}
function LiteUsageGroup(props: { lite: NonNullable<LiteSubscription> }) {
const params = useParams()
const i18n = useI18n()
const [open, setOpen] = createSignal<LiteUsageWindow>()
const [store, setStore] = createStore({
details: {} as Partial<Record<LiteUsageWindow, LiteUsageDetailsData | null>>,
loading: undefined as LiteUsageWindow | undefined,
})
const items = () =>
[
{
id: "rolling",
label: i18n.t("workspace.lite.subscription.rollingUsage"),
quotaLabel: i18n.t("workspace.lite.subscription.rollingQuota"),
usage: props.lite.rollingUsage,
},
{
id: "weekly",
label: i18n.t("workspace.lite.subscription.weeklyUsage"),
quotaLabel: i18n.t("workspace.lite.subscription.weeklyQuota"),
usage: props.lite.weeklyUsage,
},
{
id: "monthly",
label: i18n.t("workspace.lite.subscription.monthlyUsage"),
quotaLabel: i18n.t("workspace.lite.subscription.monthlyQuota"),
usage: props.lite.monthlyUsage,
},
] as const
const selected = createMemo(() => items().find((item) => item.id === open()))
async function toggle(id: LiteUsageWindow) {
if (open() === id) {
setOpen()
return
}
setOpen(id)
if (store.details[id] !== undefined) return
setStore("loading", id)
const details = await queryLiteUsageDetails(params.id!, id).catch(() => null)
setStore("details", id, details)
setStore("loading", (current) => (current === id ? undefined : current))
}
return (
<>
<div data-slot="usage">
<For each={items()}>
{(item) => (
<LiteUsageItem
id={item.id}
label={item.label}
usage={item.usage}
open={open() === item.id}
onToggle={() => toggle(item.id)}
/>
)}
</For>
</div>
<Show when={selected()}>
{(item) => {
const details = () => store.details[item().id]
return (
<Show
when={details()}
fallback={
<Show when={store.loading === item().id}>
<div data-slot="usage-details-loading">{i18n.t("workspace.lite.loading")}</div>
</Show>
}
>
{(usage) => (
<LiteUsageDetails
id={item().id}
label={item().label}
quotaLabel={item().quotaLabel}
usage={usage()}
/>
)}
</Show>
)
}}
</Show>
</>
)
}
export function LiteSection(props: { lite: LiteSubscription | undefined }) {
const params = useParams()
const i18n = useI18n()
@@ -261,11 +562,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
</a>
.
</div>
<div data-slot="usage">
<LiteUsageItem label={i18n.t("workspace.lite.subscription.rollingUsage")} usage={sub().rollingUsage} />
<LiteUsageItem label={i18n.t("workspace.lite.subscription.weeklyUsage")} usage={sub().weeklyUsage} />
<LiteUsageItem label={i18n.t("workspace.lite.subscription.monthlyUsage")} usage={sub().monthlyUsage} />
</div>
<LiteUsageGroup lite={sub()} />
<form action={setLiteUseBalance} method="post" data-slot="setting-row">
<p>{i18n.t("workspace.lite.subscription.useBalance")}</p>
<input type="hidden" name="workspaceID" value={params.id} />
@@ -1114,7 +1114,7 @@ export async function handler(
enrichment: (() => {
if (billingSource === "subscription") return { plan: "sub" }
if (billingSource === "byok") return { plan: "byok" }
if (billingSource === "lite") return { plan: "lite" }
if (billingSource === "lite") return { plan: "lite", costMultiplier: modelInfo.costMultiplier }
return undefined
})(),
}),
@@ -0,0 +1,75 @@
import { describe, expect, test } from "bun:test"
import { buildLiteUsageBreakdown, getModelQuotaLimit } from "../src/lib/lite-usage"
describe("Go usage breakdown", () => {
test("derives the model quota from the window limit and multiplier", () => {
expect(getModelQuotaLimit(30, 1)).toBe(30)
expect(getModelQuotaLimit(30, 2)).toBe(15)
expect(getModelQuotaLimit(30, 4)).toBe(7.5)
})
test("groups model quota usage into the percentage of the limit", () => {
const result = buildLiteUsageBreakdown({
usage: 416,
limit: 1_200,
sources: [
{ model: "glm", name: "GLM", cost: 200, quotaCost: 300, multiplier: 1.5, estimated: false },
{ model: "kimi", name: "Kimi", cost: 116, quotaCost: 116, multiplier: 1, estimated: false },
],
})
expect(result.usagePercent).toBe(34.7)
expect(result.rows[0]).toMatchObject({ name: "GLM", multiplier: 1.5, contributionPercent: 25 })
expect(result.rows.reduce((total, row) => total + row.contributionPercent, 0)).toBeCloseTo(result.usagePercent)
})
test("distributes credits across the model contributions", () => {
const result = buildLiteUsageBreakdown({
usage: 366,
limit: 1_200,
sources: [
{ model: "glm", name: "GLM", cost: 200, quotaCost: 300, multiplier: 1.5, estimated: false },
{ model: "kimi", name: "Kimi", cost: 116, quotaCost: 116, multiplier: 1, estimated: true },
],
})
expect(result.rows).toHaveLength(2)
expect(result.rows.every((row) => row.contributionPercent >= 0)).toBe(true)
expect(result.rows.reduce((total, row) => total + row.contributionPercent, 0)).toBeCloseTo(result.usagePercent)
})
test("does not synthesize a row when request history is unavailable", () => {
const result = buildLiteUsageBreakdown({ usage: 120, limit: 1_200, sources: [] })
expect(result.rows).toEqual([])
})
test("allocates rounded percentages without making positive rows negative", () => {
const sources = Array.from({ length: 20 }, (_, index) => ({
model: `model-${index}`,
name: `Model ${index}`,
cost: 4,
quotaCost: 4,
multiplier: 1,
estimated: false,
}))
const result = buildLiteUsageBreakdown({ usage: 80, limit: 10_000, sources })
expect(result.rows.every((row) => row.contributionPercent >= 0)).toBe(true)
expect(result.rows.reduce((total, row) => total + row.contributionPercent, 0)).toBeCloseTo(result.usagePercent)
})
test("keeps multiplier changes for the same model as separate rows", () => {
const result = buildLiteUsageBreakdown({
usage: 500,
limit: 1_000,
sources: [
{ model: "glm", name: "GLM", cost: 100, quotaCost: 100, multiplier: 1, estimated: false },
{ model: "glm", name: "GLM", cost: 200, quotaCost: 400, multiplier: 2, estimated: false },
],
})
expect(result.rows.map((row) => row.multiplier)).toEqual([2, 1])
expect(result.rows.map((row) => row.contributionPercent)).toEqual([40, 10])
})
})
@@ -129,6 +129,7 @@ export const UsageTable = mysqlTable(
sessionID: varchar("session_id", { length: 30 }),
enrichment: json("enrichment").$type<{
plan: "sub" | "byok" | "lite"
costMultiplier?: number
}>(),
},
(table) => [...workspaceIndexes(table), index("usage_time_created").on(table.workspaceID, table.timeCreated)],