mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-27 03:51:21 -04:00
zen: display quota usage breakdown
This commit is contained in:
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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」をプロバイダーとして選択してください。",
|
||||
|
||||
@@ -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"를 공급자로 선택하세요.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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": "Провайдери",
|
||||
|
||||
@@ -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 模型。",
|
||||
|
||||
@@ -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)],
|
||||
|
||||
Reference in New Issue
Block a user