diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 5520df87f..36c86ef10 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -345,6 +345,7 @@ export const dict = { "workspace.usage.breakdown.output": "الخرج", "workspace.usage.breakdown.reasoning": "المنطق", "workspace.usage.subscription": "الاشتراك (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "التكلفة", @@ -354,6 +355,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(محذوف)", "workspace.cost.empty": "لا توجد بيانات استخدام متاحة للفترة المحددة.", "workspace.cost.subscriptionShort": "اشتراك", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "مفاتيح API", "workspace.keys.subtitle": "إدارة مفاتيح API الخاصة بك للوصول إلى خدمات opencode.", @@ -481,6 +483,31 @@ export const dict = { "workspace.black.waitlist.enrolled": "مسجل", "workspace.black.waitlist.enrollNote": 'عند النقر فوق "تسجيل"، يبدأ اشتراكك على الفور وسيتم خصم الرسوم من بطاقتك.', + "workspace.lite.loading": "جارٍ التحميل...", + "workspace.lite.time.day": "يوم", + "workspace.lite.time.days": "أيام", + "workspace.lite.time.hour": "ساعة", + "workspace.lite.time.hours": "ساعات", + "workspace.lite.time.minute": "دقيقة", + "workspace.lite.time.minutes": "دقائق", + "workspace.lite.time.fewSeconds": "بضع ثوان", + "workspace.lite.subscription.title": "اشتراك Lite", + "workspace.lite.subscription.message": "أنت مشترك في OpenCode Lite.", + "workspace.lite.subscription.manage": "إدارة الاشتراك", + "workspace.lite.subscription.rollingUsage": "الاستخدام المتجدد", + "workspace.lite.subscription.weeklyUsage": "الاستخدام الأسبوعي", + "workspace.lite.subscription.monthlyUsage": "الاستخدام الشهري", + "workspace.lite.subscription.resetsIn": "إعادة تعيين في", + "workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام", + "workspace.lite.other.title": "اشتراك Lite", + "workspace.lite.other.message": + "عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Lite. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "احصل على وصول إلى أفضل النماذج المفتوحة — Kimi K2.5، و GLM-5، و MiniMax M2.5 — مع حدود استخدام سخية مقابل $10 شهريًا.", + "workspace.lite.promo.subscribe": "الاشتراك في Lite", + "workspace.lite.promo.subscribing": "جارٍ إعادة التوجيه...", + "download.title": "OpenCode | تنزيل", "download.meta.description": "نزّل OpenCode لـ macOS، Windows، وLinux", "download.hero.title": "تنزيل OpenCode", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index d03522683..5367a748b 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -350,6 +350,7 @@ export const dict = { "workspace.usage.breakdown.output": "Saída", "workspace.usage.breakdown.reasoning": "Raciocínio", "workspace.usage.subscription": "assinatura (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Custo", @@ -359,6 +360,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(excluído)", "workspace.cost.empty": "Nenhum dado de uso disponível para o período selecionado.", "workspace.cost.subscriptionShort": "ass", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "Chaves de API", "workspace.keys.subtitle": "Gerencie suas chaves de API para acessar os serviços opencode.", @@ -487,6 +489,31 @@ export const dict = { "workspace.black.waitlist.enrollNote": "Ao clicar em Inscrever-se, sua assinatura começará imediatamente e seu cartão será cobrado.", + "workspace.lite.loading": "Carregando...", + "workspace.lite.time.day": "dia", + "workspace.lite.time.days": "dias", + "workspace.lite.time.hour": "hora", + "workspace.lite.time.hours": "horas", + "workspace.lite.time.minute": "minuto", + "workspace.lite.time.minutes": "minutos", + "workspace.lite.time.fewSeconds": "alguns segundos", + "workspace.lite.subscription.title": "Assinatura Lite", + "workspace.lite.subscription.message": "Você assina o OpenCode Lite.", + "workspace.lite.subscription.manage": "Gerenciar Assinatura", + "workspace.lite.subscription.rollingUsage": "Uso Contínuo", + "workspace.lite.subscription.weeklyUsage": "Uso Semanal", + "workspace.lite.subscription.monthlyUsage": "Uso Mensal", + "workspace.lite.subscription.resetsIn": "Reinicia em", + "workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso", + "workspace.lite.other.title": "Assinatura Lite", + "workspace.lite.other.message": + "Outro membro neste workspace já assina o OpenCode Lite. Apenas um membro por workspace pode assinar.", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "Tenha acesso aos melhores modelos abertos — Kimi K2.5, GLM-5 e MiniMax M2.5 — com limites de uso generosos por $10 por mês.", + "workspace.lite.promo.subscribe": "Assinar Lite", + "workspace.lite.promo.subscribing": "Redirecionando...", + "download.title": "OpenCode | Baixar", "download.meta.description": "Baixe o OpenCode para macOS, Windows e Linux", "download.hero.title": "Baixar OpenCode", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index 94826c438..2f1be69ca 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -348,6 +348,7 @@ export const dict = { "workspace.usage.breakdown.output": "Output", "workspace.usage.breakdown.reasoning": "Ræsonnement", "workspace.usage.subscription": "abonnement (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Omkostninger", @@ -357,6 +358,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(slettet)", "workspace.cost.empty": "Ingen brugsdata tilgængelige for den valgte periode.", "workspace.cost.subscriptionShort": "sub", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "API-nøgler", "workspace.keys.subtitle": "Administrer dine API-nøgler for at få adgang til opencode-tjenester.", @@ -485,6 +487,31 @@ export const dict = { "workspace.black.waitlist.enrollNote": "Når du klikker på Tilmeld, starter dit abonnement med det samme, og dit kort vil blive debiteret.", + "workspace.lite.loading": "Indlæser...", + "workspace.lite.time.day": "dag", + "workspace.lite.time.days": "dage", + "workspace.lite.time.hour": "time", + "workspace.lite.time.hours": "timer", + "workspace.lite.time.minute": "minut", + "workspace.lite.time.minutes": "minutter", + "workspace.lite.time.fewSeconds": "et par sekunder", + "workspace.lite.subscription.title": "Lite-abonnement", + "workspace.lite.subscription.message": "Du abonnerer på OpenCode Lite.", + "workspace.lite.subscription.manage": "Administrer abonnement", + "workspace.lite.subscription.rollingUsage": "Løbende forbrug", + "workspace.lite.subscription.weeklyUsage": "Ugentligt forbrug", + "workspace.lite.subscription.monthlyUsage": "Månedligt forbrug", + "workspace.lite.subscription.resetsIn": "Nulstiller i", + "workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne", + "workspace.lite.other.title": "Lite-abonnement", + "workspace.lite.other.message": + "Et andet medlem i dette workspace abonnerer allerede på OpenCode Lite. Kun ét medlem pr. workspace kan abonnere.", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "Få adgang til de bedste åbne modeller — Kimi K2.5, GLM-5 og MiniMax M2.5 — med generøse forbrugsgrænser for $10 om måneden.", + "workspace.lite.promo.subscribe": "Abonner på Lite", + "workspace.lite.promo.subscribing": "Omdirigerer...", + "download.title": "OpenCode | Download", "download.meta.description": "Download OpenCode til macOS, Windows og Linux", "download.hero.title": "Download OpenCode", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index cfa207066..49df65f8d 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -350,6 +350,7 @@ export const dict = { "workspace.usage.breakdown.output": "Output", "workspace.usage.breakdown.reasoning": "Reasoning", "workspace.usage.subscription": "Abonnement (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Kosten", @@ -359,6 +360,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(gelöscht)", "workspace.cost.empty": "Keine Nutzungsdaten für den gewählten Zeitraum verfügbar.", "workspace.cost.subscriptionShort": "Abo", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "API Keys", "workspace.keys.subtitle": "Verwalte deine API Keys für den Zugriff auf OpenCode-Dienste.", @@ -487,6 +489,31 @@ export const dict = { "workspace.black.waitlist.enrollNote": "Wenn du auf Einschreiben klickst, startet dein Abo sofort und deine Karte wird belastet.", + "workspace.lite.loading": "Lade...", + "workspace.lite.time.day": "Tag", + "workspace.lite.time.days": "Tage", + "workspace.lite.time.hour": "Stunde", + "workspace.lite.time.hours": "Stunden", + "workspace.lite.time.minute": "Minute", + "workspace.lite.time.minutes": "Minuten", + "workspace.lite.time.fewSeconds": "einige Sekunden", + "workspace.lite.subscription.title": "Lite-Abonnement", + "workspace.lite.subscription.message": "Du hast OpenCode Lite abonniert.", + "workspace.lite.subscription.manage": "Abo verwalten", + "workspace.lite.subscription.rollingUsage": "Fortlaufende Nutzung", + "workspace.lite.subscription.weeklyUsage": "Wöchentliche Nutzung", + "workspace.lite.subscription.monthlyUsage": "Monatliche Nutzung", + "workspace.lite.subscription.resetsIn": "Setzt zurück in", + "workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind", + "workspace.lite.other.title": "Lite-Abonnement", + "workspace.lite.other.message": + "Ein anderes Mitglied in diesem Workspace hat OpenCode Lite bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "Erhalte Zugriff auf die besten offenen Modelle — Kimi K2.5, GLM-5 und MiniMax M2.5 — mit großzügigen Nutzungslimits für $10 pro Monat.", + "workspace.lite.promo.subscribe": "Lite abonnieren", + "workspace.lite.promo.subscribing": "Leite weiter...", + "download.title": "OpenCode | Download", "download.meta.description": "Lade OpenCode für macOS, Windows und Linux herunter", "download.hero.title": "OpenCode herunterladen", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index 8e096dd5c..42b88dd16 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -342,6 +342,7 @@ export const dict = { "workspace.usage.breakdown.output": "Output", "workspace.usage.breakdown.reasoning": "Reasoning", "workspace.usage.subscription": "subscription (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Cost", @@ -351,6 +352,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(deleted)", "workspace.cost.empty": "No usage data available for the selected period.", "workspace.cost.subscriptionShort": "sub", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "API Keys", "workspace.keys.subtitle": "Manage your API keys for accessing opencode services.", @@ -479,6 +481,31 @@ export const dict = { "workspace.black.waitlist.enrollNote": "When you click Enroll, your subscription starts immediately and your card will be charged.", + "workspace.lite.loading": "Loading...", + "workspace.lite.time.day": "day", + "workspace.lite.time.days": "days", + "workspace.lite.time.hour": "hour", + "workspace.lite.time.hours": "hours", + "workspace.lite.time.minute": "minute", + "workspace.lite.time.minutes": "minutes", + "workspace.lite.time.fewSeconds": "a few seconds", + "workspace.lite.subscription.title": "Lite Subscription", + "workspace.lite.subscription.message": "You are subscribed to OpenCode Lite.", + "workspace.lite.subscription.manage": "Manage Subscription", + "workspace.lite.subscription.rollingUsage": "Rolling Usage", + "workspace.lite.subscription.weeklyUsage": "Weekly Usage", + "workspace.lite.subscription.monthlyUsage": "Monthly Usage", + "workspace.lite.subscription.resetsIn": "Resets in", + "workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits", + "workspace.lite.other.title": "Lite Subscription", + "workspace.lite.other.message": + "Another member in this workspace is already subscribed to OpenCode Lite. Only one member per workspace can subscribe.", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "Get access to the best open models — Kimi K2.5, GLM-5, and MiniMax M2.5 — with generous usage limits for $10 per month.", + "workspace.lite.promo.subscribe": "Subscribe to Lite", + "workspace.lite.promo.subscribing": "Redirecting...", + "download.title": "OpenCode | Download", "download.meta.description": "Download OpenCode for macOS, Windows, and Linux", "download.hero.title": "Download OpenCode", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index c8579462e..f4ac1cc63 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -351,6 +351,7 @@ export const dict = { "workspace.usage.breakdown.output": "Salida", "workspace.usage.breakdown.reasoning": "Razonamiento", "workspace.usage.subscription": "suscripción (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Costo", @@ -360,6 +361,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(eliminado)", "workspace.cost.empty": "No hay datos de uso disponibles para el periodo seleccionado.", "workspace.cost.subscriptionShort": "sub", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "Claves API", "workspace.keys.subtitle": "Gestiona tus claves API para acceder a los servicios de opencode.", @@ -488,6 +490,31 @@ export const dict = { "workspace.black.waitlist.enrollNote": "Cuando haces clic en Inscribirse, tu suscripción comienza inmediatamente y se cargará a tu tarjeta.", + "workspace.lite.loading": "Cargando...", + "workspace.lite.time.day": "día", + "workspace.lite.time.days": "días", + "workspace.lite.time.hour": "hora", + "workspace.lite.time.hours": "horas", + "workspace.lite.time.minute": "minuto", + "workspace.lite.time.minutes": "minutos", + "workspace.lite.time.fewSeconds": "unos pocos segundos", + "workspace.lite.subscription.title": "Suscripción Lite", + "workspace.lite.subscription.message": "Estás suscrito a OpenCode Lite.", + "workspace.lite.subscription.manage": "Gestionar Suscripción", + "workspace.lite.subscription.rollingUsage": "Uso Continuo", + "workspace.lite.subscription.weeklyUsage": "Uso Semanal", + "workspace.lite.subscription.monthlyUsage": "Uso Mensual", + "workspace.lite.subscription.resetsIn": "Se reinicia en", + "workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso", + "workspace.lite.other.title": "Suscripción Lite", + "workspace.lite.other.message": + "Otro miembro de este espacio de trabajo ya está suscrito a OpenCode Lite. Solo un miembro por espacio de trabajo puede suscribirse.", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "Obtén acceso a los mejores modelos abiertos — Kimi K2.5, GLM-5 y MiniMax M2.5 — con generosos límites de uso por $10 al mes.", + "workspace.lite.promo.subscribe": "Suscribirse a Lite", + "workspace.lite.promo.subscribing": "Redirigiendo...", + "download.title": "OpenCode | Descargar", "download.meta.description": "Descarga OpenCode para macOS, Windows y Linux", "download.hero.title": "Descargar OpenCode", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index ccb0a8cc6..05ee4e843 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -356,6 +356,7 @@ export const dict = { "workspace.usage.breakdown.output": "Sortie", "workspace.usage.breakdown.reasoning": "Raisonnement", "workspace.usage.subscription": "abonnement ({{amount}} $)", + "workspace.usage.lite": "lite ({{amount}} $)", "workspace.usage.byok": "BYOK ({{amount}} $)", "workspace.cost.title": "Coût", @@ -365,6 +366,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(supprimé)", "workspace.cost.empty": "Aucune donnée d'utilisation disponible pour la période sélectionnée.", "workspace.cost.subscriptionShort": "abo", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "Clés API", "workspace.keys.subtitle": "Gérez vos clés API pour accéder aux services OpenCode.", @@ -496,6 +498,32 @@ export const dict = { "workspace.black.waitlist.enrollNote": "Lorsque vous cliquez sur S'inscrire, votre abonnement démarre immédiatement et votre carte sera débitée.", + "workspace.lite.loading": "Chargement...", + "workspace.lite.time.day": "jour", + "workspace.lite.time.days": "jours", + "workspace.lite.time.hour": "heure", + "workspace.lite.time.hours": "heures", + "workspace.lite.time.minute": "minute", + "workspace.lite.time.minutes": "minutes", + "workspace.lite.time.fewSeconds": "quelques secondes", + "workspace.lite.subscription.title": "Abonnement Lite", + "workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Lite.", + "workspace.lite.subscription.manage": "Gérer l'abonnement", + "workspace.lite.subscription.rollingUsage": "Utilisation glissante", + "workspace.lite.subscription.weeklyUsage": "Utilisation hebdomadaire", + "workspace.lite.subscription.monthlyUsage": "Utilisation mensuelle", + "workspace.lite.subscription.resetsIn": "Réinitialisation dans", + "workspace.lite.subscription.useBalance": + "Utilisez votre solde disponible après avoir atteint les limites d'utilisation", + "workspace.lite.other.title": "Abonnement Lite", + "workspace.lite.other.message": + "Un autre membre de cet espace de travail est déjà abonné à OpenCode Lite. Un seul membre par espace de travail peut s'abonner.", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "Accédez aux meilleurs modèles ouverts — Kimi K2.5, GLM-5 et MiniMax M2.5 — avec des limites d'utilisation généreuses pour 10 $ par mois.", + "workspace.lite.promo.subscribe": "S'abonner à Lite", + "workspace.lite.promo.subscribing": "Redirection...", + "download.title": "OpenCode | Téléchargement", "download.meta.description": "Téléchargez OpenCode pour macOS, Windows et Linux", "download.hero.title": "Télécharger OpenCode", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index 21162f699..08b795504 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -350,6 +350,7 @@ export const dict = { "workspace.usage.breakdown.output": "Output", "workspace.usage.breakdown.reasoning": "Reasoning", "workspace.usage.subscription": "abbonamento (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Costo", @@ -359,6 +360,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(eliminato)", "workspace.cost.empty": "Nessun dato di utilizzo disponibile per il periodo selezionato.", "workspace.cost.subscriptionShort": "sub", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "Chiavi API", "workspace.keys.subtitle": "Gestisci le tue chiavi API per accedere ai servizi opencode.", @@ -487,6 +489,31 @@ export const dict = { "workspace.black.waitlist.enrollNote": "Quando clicchi su Iscriviti, il tuo abbonamento inizia immediatamente e la tua carta verrà addebitata.", + "workspace.lite.loading": "Caricamento...", + "workspace.lite.time.day": "giorno", + "workspace.lite.time.days": "giorni", + "workspace.lite.time.hour": "ora", + "workspace.lite.time.hours": "ore", + "workspace.lite.time.minute": "minuto", + "workspace.lite.time.minutes": "minuti", + "workspace.lite.time.fewSeconds": "pochi secondi", + "workspace.lite.subscription.title": "Abbonamento Lite", + "workspace.lite.subscription.message": "Sei abbonato a OpenCode Lite.", + "workspace.lite.subscription.manage": "Gestisci Abbonamento", + "workspace.lite.subscription.rollingUsage": "Utilizzo Continuativo", + "workspace.lite.subscription.weeklyUsage": "Utilizzo Settimanale", + "workspace.lite.subscription.monthlyUsage": "Utilizzo Mensile", + "workspace.lite.subscription.resetsIn": "Si resetta tra", + "workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo", + "workspace.lite.other.title": "Abbonamento Lite", + "workspace.lite.other.message": + "Un altro membro in questo workspace è già abbonato a OpenCode Lite. Solo un membro per workspace può abbonarsi.", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "Ottieni l'accesso ai migliori modelli aperti — Kimi K2.5, GLM-5 e MiniMax M2.5 — con limiti di utilizzo generosi per $10 al mese.", + "workspace.lite.promo.subscribe": "Abbonati a Lite", + "workspace.lite.promo.subscribing": "Reindirizzamento...", + "download.title": "OpenCode | Download", "download.meta.description": "Scarica OpenCode per macOS, Windows e Linux", "download.hero.title": "Scarica OpenCode", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index 1f2746f2b..2c8e9d6b4 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -347,6 +347,7 @@ export const dict = { "workspace.usage.breakdown.output": "出力", "workspace.usage.breakdown.reasoning": "推論", "workspace.usage.subscription": "サブスクリプション (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "コスト", @@ -356,6 +357,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(削除済み)", "workspace.cost.empty": "選択した期間の使用状況データはありません。", "workspace.cost.subscriptionShort": "サブ", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "APIキー", "workspace.keys.subtitle": "OpenCodeサービスにアクセスするためのAPIキーを管理します。", @@ -485,6 +487,31 @@ export const dict = { "workspace.black.waitlist.enrollNote": "「登録する」をクリックすると、サブスクリプションがすぐに開始され、カードに請求されます。", + "workspace.lite.loading": "読み込み中...", + "workspace.lite.time.day": "日", + "workspace.lite.time.days": "日", + "workspace.lite.time.hour": "時間", + "workspace.lite.time.hours": "時間", + "workspace.lite.time.minute": "分", + "workspace.lite.time.minutes": "分", + "workspace.lite.time.fewSeconds": "数秒", + "workspace.lite.subscription.title": "Liteサブスクリプション", + "workspace.lite.subscription.message": "あなたは OpenCode Lite を購読しています。", + "workspace.lite.subscription.manage": "サブスクリプションの管理", + "workspace.lite.subscription.rollingUsage": "ローリング利用量", + "workspace.lite.subscription.weeklyUsage": "週間利用量", + "workspace.lite.subscription.monthlyUsage": "月間利用量", + "workspace.lite.subscription.resetsIn": "リセットまで", + "workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する", + "workspace.lite.other.title": "Liteサブスクリプション", + "workspace.lite.other.message": + "このワークスペースの別のメンバーが既に OpenCode Lite を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "月額$10で、十分な利用枠が設けられた最高のオープンモデル — Kimi K2.5、GLM-5、および MiniMax M2.5 — にアクセスできます。", + "workspace.lite.promo.subscribe": "Liteを購読する", + "workspace.lite.promo.subscribing": "リダイレクト中...", + "download.title": "OpenCode | ダウンロード", "download.meta.description": "OpenCode を macOS、Windows、Linux 向けにダウンロード", "download.hero.title": "OpenCode をダウンロード", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index 5a5f9bc71..8f4e58e7d 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -344,6 +344,7 @@ export const dict = { "workspace.usage.breakdown.output": "출력", "workspace.usage.breakdown.reasoning": "추론", "workspace.usage.subscription": "구독 (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "비용", @@ -353,6 +354,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(삭제됨)", "workspace.cost.empty": "선택한 기간에 사용 데이터가 없습니다.", "workspace.cost.subscriptionShort": "구독", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "API 키", "workspace.keys.subtitle": "OpenCode 서비스 액세스를 위한 API 키를 관리하세요.", @@ -480,6 +482,31 @@ export const dict = { "workspace.black.waitlist.enrolled": "등록됨", "workspace.black.waitlist.enrollNote": "등록을 클릭하면 구독이 즉시 시작되며 카드에 요금이 청구됩니다.", + "workspace.lite.loading": "로드 중...", + "workspace.lite.time.day": "일", + "workspace.lite.time.days": "일", + "workspace.lite.time.hour": "시간", + "workspace.lite.time.hours": "시간", + "workspace.lite.time.minute": "분", + "workspace.lite.time.minutes": "분", + "workspace.lite.time.fewSeconds": "몇 초", + "workspace.lite.subscription.title": "Lite 구독", + "workspace.lite.subscription.message": "현재 OpenCode Lite를 구독 중입니다.", + "workspace.lite.subscription.manage": "구독 관리", + "workspace.lite.subscription.rollingUsage": "롤링 사용량", + "workspace.lite.subscription.weeklyUsage": "주간 사용량", + "workspace.lite.subscription.monthlyUsage": "월간 사용량", + "workspace.lite.subscription.resetsIn": "초기화까지 남은 시간:", + "workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용", + "workspace.lite.other.title": "Lite 구독", + "workspace.lite.other.message": + "이 워크스페이스의 다른 멤버가 이미 OpenCode Lite를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "월 $10의 넉넉한 사용 한도로 최고의 오픈 모델인 Kimi K2.5, GLM-5, MiniMax M2.5에 액세스하세요.", + "workspace.lite.promo.subscribe": "Lite 구독하기", + "workspace.lite.promo.subscribing": "리디렉션 중...", + "download.title": "OpenCode | 다운로드", "download.meta.description": "macOS, Windows, Linux용 OpenCode 다운로드", "download.hero.title": "OpenCode 다운로드", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index abd9ba086..e5bfef989 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -348,6 +348,7 @@ export const dict = { "workspace.usage.breakdown.output": "Output", "workspace.usage.breakdown.reasoning": "Resonnering", "workspace.usage.subscription": "abonnement (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Kostnad", @@ -357,6 +358,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(slettet)", "workspace.cost.empty": "Ingen bruksdata tilgjengelig for den valgte perioden.", "workspace.cost.subscriptionShort": "sub", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "API-nøkler", "workspace.keys.subtitle": "Administrer API-nøklene dine for å få tilgang til opencode-tjenester.", @@ -485,6 +487,31 @@ export const dict = { "workspace.black.waitlist.enrollNote": "Når du klikker på Meld på, starter abonnementet umiddelbart og kortet ditt belastes.", + "workspace.lite.loading": "Laster...", + "workspace.lite.time.day": "dag", + "workspace.lite.time.days": "dager", + "workspace.lite.time.hour": "time", + "workspace.lite.time.hours": "timer", + "workspace.lite.time.minute": "minutt", + "workspace.lite.time.minutes": "minutter", + "workspace.lite.time.fewSeconds": "noen få sekunder", + "workspace.lite.subscription.title": "Lite-abonnement", + "workspace.lite.subscription.message": "Du abonnerer på OpenCode Lite.", + "workspace.lite.subscription.manage": "Administrer abonnement", + "workspace.lite.subscription.rollingUsage": "Løpende bruk", + "workspace.lite.subscription.weeklyUsage": "Ukentlig bruk", + "workspace.lite.subscription.monthlyUsage": "Månedlig bruk", + "workspace.lite.subscription.resetsIn": "Nullstilles om", + "workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene", + "workspace.lite.other.title": "Lite-abonnement", + "workspace.lite.other.message": + "Et annet medlem i dette arbeidsområdet abonnerer allerede på OpenCode Lite. Kun ett medlem per arbeidsområde kan abonnere.", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "Få tilgang til de beste åpne modellene — Kimi K2.5, GLM-5 og MiniMax M2.5 — med generøse bruksgrenser for $10 per måned.", + "workspace.lite.promo.subscribe": "Abonner på Lite", + "workspace.lite.promo.subscribing": "Omdirigerer...", + "download.title": "OpenCode | Last ned", "download.meta.description": "Last ned OpenCode for macOS, Windows og Linux", "download.hero.title": "Last ned OpenCode", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index eeb7ce2b0..c2f9b3712 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -349,6 +349,7 @@ export const dict = { "workspace.usage.breakdown.output": "Wyjście", "workspace.usage.breakdown.reasoning": "Rozumowanie", "workspace.usage.subscription": "subskrypcja (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Koszt", @@ -358,6 +359,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(usunięte)", "workspace.cost.empty": "Brak danych o użyciu dla wybranego okresu.", "workspace.cost.subscriptionShort": "sub", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "Klucze API", "workspace.keys.subtitle": "Zarządzaj kluczami API do usług opencode.", @@ -486,6 +488,31 @@ export const dict = { "workspace.black.waitlist.enrollNote": "Po kliknięciu Zapisz się, Twoja subskrypcja rozpocznie się natychmiast, a karta zostanie obciążona.", + "workspace.lite.loading": "Ładowanie...", + "workspace.lite.time.day": "dzień", + "workspace.lite.time.days": "dni", + "workspace.lite.time.hour": "godzina", + "workspace.lite.time.hours": "godzin(y)", + "workspace.lite.time.minute": "minuta", + "workspace.lite.time.minutes": "minut(y)", + "workspace.lite.time.fewSeconds": "kilka sekund", + "workspace.lite.subscription.title": "Subskrypcja Lite", + "workspace.lite.subscription.message": "Subskrybujesz OpenCode Lite.", + "workspace.lite.subscription.manage": "Zarządzaj subskrypcją", + "workspace.lite.subscription.rollingUsage": "Użycie kroczące", + "workspace.lite.subscription.weeklyUsage": "Użycie tygodniowe", + "workspace.lite.subscription.monthlyUsage": "Użycie miesięczne", + "workspace.lite.subscription.resetsIn": "Resetuje się za", + "workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia", + "workspace.lite.other.title": "Subskrypcja Lite", + "workspace.lite.other.message": + "Inny członek tego obszaru roboczego już subskrybuje OpenCode Lite. Tylko jeden członek na obszar roboczy może subskrybować.", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "Uzyskaj dostęp do najlepszych otwartych modeli — Kimi K2.5, GLM-5 i MiniMax M2.5 — z hojnymi limitami użycia za $10 miesięcznie.", + "workspace.lite.promo.subscribe": "Subskrybuj Lite", + "workspace.lite.promo.subscribing": "Przekierowywanie...", + "download.title": "OpenCode | Pobierz", "download.meta.description": "Pobierz OpenCode na macOS, Windows i Linux", "download.hero.title": "Pobierz OpenCode", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index 1f752fd59..3bedf80b5 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -354,6 +354,7 @@ export const dict = { "workspace.usage.breakdown.output": "Выход", "workspace.usage.breakdown.reasoning": "Reasoning (рассуждения)", "workspace.usage.subscription": "подписка (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Расходы", @@ -363,6 +364,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(удалено)", "workspace.cost.empty": "Нет данных об использовании за выбранный период.", "workspace.cost.subscriptionShort": "подписка", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "API Ключи", "workspace.keys.subtitle": "Управляйте вашими API ключами для доступа к сервисам opencode.", @@ -491,6 +493,31 @@ export const dict = { "workspace.black.waitlist.enrollNote": "Когда вы нажмете Подключиться, ваша подписка начнется немедленно, и с карты будет списана оплата.", + "workspace.lite.loading": "Загрузка...", + "workspace.lite.time.day": "день", + "workspace.lite.time.days": "дней", + "workspace.lite.time.hour": "час", + "workspace.lite.time.hours": "часов", + "workspace.lite.time.minute": "минута", + "workspace.lite.time.minutes": "минут", + "workspace.lite.time.fewSeconds": "несколько секунд", + "workspace.lite.subscription.title": "Подписка Lite", + "workspace.lite.subscription.message": "Вы подписаны на OpenCode Lite.", + "workspace.lite.subscription.manage": "Управление подпиской", + "workspace.lite.subscription.rollingUsage": "Скользящее использование", + "workspace.lite.subscription.weeklyUsage": "Недельное использование", + "workspace.lite.subscription.monthlyUsage": "Ежемесячное использование", + "workspace.lite.subscription.resetsIn": "Сброс через", + "workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов", + "workspace.lite.other.title": "Подписка Lite", + "workspace.lite.other.message": + "Другой участник в этом рабочем пространстве уже подписан на OpenCode Lite. Только один участник в рабочем пространстве может оформить подписку.", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "Получите доступ к лучшим открытым моделям — Kimi K2.5, GLM-5 и MiniMax M2.5 — с щедрыми лимитами использования за $10 в месяц.", + "workspace.lite.promo.subscribe": "Подписаться на Lite", + "workspace.lite.promo.subscribing": "Перенаправление...", + "download.title": "OpenCode | Скачать", "download.meta.description": "Скачать OpenCode для macOS, Windows и Linux", "download.hero.title": "Скачать OpenCode", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index 8196bd9c1..3d36dcbb2 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -347,6 +347,7 @@ export const dict = { "workspace.usage.breakdown.output": "Output", "workspace.usage.breakdown.reasoning": "Reasoning", "workspace.usage.subscription": "สมัครสมาชิก (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "ค่าใช้จ่าย", @@ -356,6 +357,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(ลบแล้ว)", "workspace.cost.empty": "ไม่มีข้อมูลการใช้งานในช่วงเวลาที่เลือก", "workspace.cost.subscriptionShort": "sub", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "API Keys", "workspace.keys.subtitle": "จัดการ API keys ของคุณสำหรับการเข้าถึงบริการ OpenCode", @@ -484,6 +486,31 @@ export const dict = { "workspace.black.waitlist.enrollNote": "เมื่อคุณคลิกลงทะเบียน การสมัครสมาชิกของคุณจะเริ่มต้นทันทีและบัตรของคุณจะถูกเรียกเก็บเงิน", + "workspace.lite.loading": "กำลังโหลด...", + "workspace.lite.time.day": "วัน", + "workspace.lite.time.days": "วัน", + "workspace.lite.time.hour": "ชั่วโมง", + "workspace.lite.time.hours": "ชั่วโมง", + "workspace.lite.time.minute": "นาที", + "workspace.lite.time.minutes": "นาที", + "workspace.lite.time.fewSeconds": "ไม่กี่วินาที", + "workspace.lite.subscription.title": "การสมัครสมาชิก Lite", + "workspace.lite.subscription.message": "คุณได้สมัครสมาชิก OpenCode Lite แล้ว", + "workspace.lite.subscription.manage": "จัดการการสมัครสมาชิก", + "workspace.lite.subscription.rollingUsage": "การใช้งานแบบหมุนเวียน", + "workspace.lite.subscription.weeklyUsage": "การใช้งานรายสัปดาห์", + "workspace.lite.subscription.monthlyUsage": "การใช้งานรายเดือน", + "workspace.lite.subscription.resetsIn": "รีเซ็ตใน", + "workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน", + "workspace.lite.other.title": "การสมัครสมาชิก Lite", + "workspace.lite.other.message": + "สมาชิกคนอื่นใน Workspace นี้ได้สมัคร OpenCode Lite แล้ว สามารถสมัครได้เพียงหนึ่งคนต่อหนึ่ง Workspace เท่านั้น", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "เข้าถึงโมเดลเปิดที่ดีที่สุด — Kimi K2.5, GLM-5 และ MiniMax M2.5 — พร้อมขีดจำกัดการใช้งานมากมายในราคา $10 ต่อเดือน", + "workspace.lite.promo.subscribe": "สมัครสมาชิก Lite", + "workspace.lite.promo.subscribing": "กำลังเปลี่ยนเส้นทาง...", + "download.title": "OpenCode | ดาวน์โหลด", "download.meta.description": "ดาวน์โหลด OpenCode สำหรับ macOS, Windows และ Linux", "download.hero.title": "ดาวน์โหลด OpenCode", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 7ee8b6a75..bfa7d09ae 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -350,6 +350,7 @@ export const dict = { "workspace.usage.breakdown.output": "Çıkış", "workspace.usage.breakdown.reasoning": "Muhakeme", "workspace.usage.subscription": "abonelik (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Maliyet", @@ -359,6 +360,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(silindi)", "workspace.cost.empty": "Seçilen döneme ait kullanım verisi yok.", "workspace.cost.subscriptionShort": "abonelik", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "API Anahtarları", "workspace.keys.subtitle": "opencode hizmetlerine erişim için API anahtarlarınızı yönetin.", @@ -487,6 +489,31 @@ export const dict = { "workspace.black.waitlist.enrollNote": "Kayıt Ol'a tıkladığınızda aboneliğiniz hemen başlar ve kartınızdan çekim yapılır.", + "workspace.lite.loading": "Yükleniyor...", + "workspace.lite.time.day": "gün", + "workspace.lite.time.days": "gün", + "workspace.lite.time.hour": "saat", + "workspace.lite.time.hours": "saat", + "workspace.lite.time.minute": "dakika", + "workspace.lite.time.minutes": "dakika", + "workspace.lite.time.fewSeconds": "birkaç saniye", + "workspace.lite.subscription.title": "Lite Aboneliği", + "workspace.lite.subscription.message": "OpenCode Lite abonesisiniz.", + "workspace.lite.subscription.manage": "Aboneliği Yönet", + "workspace.lite.subscription.rollingUsage": "Devam Eden Kullanım", + "workspace.lite.subscription.weeklyUsage": "Haftalık Kullanım", + "workspace.lite.subscription.monthlyUsage": "Aylık Kullanım", + "workspace.lite.subscription.resetsIn": "Sıfırlama süresi", + "workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın", + "workspace.lite.other.title": "Lite Aboneliği", + "workspace.lite.other.message": + "Bu çalışma alanındaki başka bir üye zaten OpenCode Lite abonesi. Çalışma alanı başına yalnızca bir üye abone olabilir.", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "Ayda $10 karşılığında cömert kullanım limitleriyle en iyi açık modellere — Kimi K2.5, GLM-5 ve MiniMax M2.5 — erişin.", + "workspace.lite.promo.subscribe": "Lite'a Abone Ol", + "workspace.lite.promo.subscribing": "Yönlendiriliyor...", + "download.title": "OpenCode | İndir", "download.meta.description": "OpenCode'u macOS, Windows ve Linux için indirin", "download.hero.title": "OpenCode'u İndir", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index e5fae6f3f..2c41be7cf 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -335,6 +335,7 @@ export const dict = { "workspace.usage.breakdown.output": "输出", "workspace.usage.breakdown.reasoning": "推理", "workspace.usage.subscription": "订阅 (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "成本", @@ -344,6 +345,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(已删除)", "workspace.cost.empty": "所选期间无可用使用数据。", "workspace.cost.subscriptionShort": "订阅", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "API 密钥", "workspace.keys.subtitle": "管理访问 OpenCode 服务的 API 密钥。", @@ -471,6 +473,30 @@ export const dict = { "workspace.black.waitlist.enrolled": "已加入", "workspace.black.waitlist.enrollNote": "点击加入后,您的订阅将立即开始,并将从您的卡中扣费。", + "workspace.lite.loading": "加载中...", + "workspace.lite.time.day": "天", + "workspace.lite.time.days": "天", + "workspace.lite.time.hour": "小时", + "workspace.lite.time.hours": "小时", + "workspace.lite.time.minute": "分钟", + "workspace.lite.time.minutes": "分钟", + "workspace.lite.time.fewSeconds": "几秒钟", + "workspace.lite.subscription.title": "Lite 订阅", + "workspace.lite.subscription.message": "您已订阅 OpenCode Lite。", + "workspace.lite.subscription.manage": "管理订阅", + "workspace.lite.subscription.rollingUsage": "滚动用量", + "workspace.lite.subscription.weeklyUsage": "每周用量", + "workspace.lite.subscription.monthlyUsage": "每月用量", + "workspace.lite.subscription.resetsIn": "重置于", + "workspace.lite.subscription.useBalance": "达到使用限额后使用您的可用余额", + "workspace.lite.other.title": "Lite 订阅", + "workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Lite。每个工作区只有一名成员可以订阅。", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "每月仅需 $10 即可访问最优秀的开源模型 — Kimi K2.5, GLM-5, 和 MiniMax M2.5 — 并享受充裕的使用限额。", + "workspace.lite.promo.subscribe": "订阅 Lite", + "workspace.lite.promo.subscribing": "正在重定向...", + "download.title": "OpenCode | 下载", "download.meta.description": "下载适用于 macOS, Windows, 和 Linux 的 OpenCode", "download.hero.title": "下载 OpenCode", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 79582b0ce..87fcaa8e8 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -335,6 +335,7 @@ export const dict = { "workspace.usage.breakdown.output": "輸出", "workspace.usage.breakdown.reasoning": "推理", "workspace.usage.subscription": "訂閱 (${{amount}})", + "workspace.usage.lite": "lite (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "成本", @@ -344,6 +345,7 @@ export const dict = { "workspace.cost.deletedSuffix": "(已刪除)", "workspace.cost.empty": "所選期間沒有可用的使用資料。", "workspace.cost.subscriptionShort": "訂", + "workspace.cost.liteShort": "lite", "workspace.keys.title": "API 金鑰", "workspace.keys.subtitle": "管理你的 API 金鑰以存取 OpenCode 服務。", @@ -471,6 +473,30 @@ export const dict = { "workspace.black.waitlist.enrolled": "已加入", "workspace.black.waitlist.enrollNote": "當你點選「加入」後,你的訂閱將立即開始,並且將從你的卡片中扣款。", + "workspace.lite.loading": "載入中...", + "workspace.lite.time.day": "天", + "workspace.lite.time.days": "天", + "workspace.lite.time.hour": "小時", + "workspace.lite.time.hours": "小時", + "workspace.lite.time.minute": "分鐘", + "workspace.lite.time.minutes": "分鐘", + "workspace.lite.time.fewSeconds": "幾秒", + "workspace.lite.subscription.title": "Lite 訂閱", + "workspace.lite.subscription.message": "您已訂閱 OpenCode Lite。", + "workspace.lite.subscription.manage": "管理訂閱", + "workspace.lite.subscription.rollingUsage": "滾動使用量", + "workspace.lite.subscription.weeklyUsage": "每週使用量", + "workspace.lite.subscription.monthlyUsage": "每月使用量", + "workspace.lite.subscription.resetsIn": "重置時間:", + "workspace.lite.subscription.useBalance": "達到使用限制後使用您的可用餘額", + "workspace.lite.other.title": "Lite 訂閱", + "workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Lite。每個工作區只能有一位成員訂閱。", + "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.promo.description": + "每月只需 $10 即可使用最佳的開放模型 — Kimi K2.5、GLM-5 和 MiniMax M2.5 — 並享有慷慨的使用限制。", + "workspace.lite.promo.subscribe": "訂閱 Lite", + "workspace.lite.promo.subscribing": "重新導向中...", + "download.title": "OpenCode | 下載", "download.meta.description": "下載適用於 macOS、Windows 與 Linux 的 OpenCode", "download.hero.title": "下載 OpenCode", diff --git a/packages/console/app/src/routes/stripe/webhook.ts b/packages/console/app/src/routes/stripe/webhook.ts index 828eb4c71..47ca442ec 100644 --- a/packages/console/app/src/routes/stripe/webhook.ts +++ b/packages/console/app/src/routes/stripe/webhook.ts @@ -1,13 +1,13 @@ import { Billing } from "@opencode-ai/console-core/billing.js" import type { APIEvent } from "@solidjs/start/server" -import { and, Database, eq, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js" -import { BillingTable, PaymentTable, SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js" +import { and, Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js" +import { BillingTable, LiteTable, PaymentTable } from "@opencode-ai/console-core/schema/billing.sql.js" import { Identifier } from "@opencode-ai/console-core/identifier.js" import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js" import { Actor } from "@opencode-ai/console-core/actor.js" import { Resource } from "@opencode-ai/console-resource" -import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" -import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js" +import { LiteData } from "@opencode-ai/console-core/lite.js" +import { BlackData } from "@opencode-ai/console-core/black.js" export async function POST(input: APIEvent) { const body = await Billing.stripe().webhooks.constructEventAsync( @@ -103,310 +103,93 @@ export async function POST(input: APIEvent) { }) }) } - if (body.type === "checkout.session.completed" && body.data.object.mode === "subscription") { - const workspaceID = body.data.object.custom_fields.find((f) => f.key === "workspaceid")?.text?.value - const amountInCents = body.data.object.amount_total as number - const customerID = body.data.object.customer as string - const customerEmail = body.data.object.customer_details?.email as string - const invoiceID = body.data.object.invoice as string - const subscriptionID = body.data.object.subscription as string - const promoCode = body.data.object.discounts?.[0]?.promotion_code as string + if (body.type === "customer.subscription.created") { + const type = body.data.object.metadata?.type + if (type === "lite") { + const workspaceID = body.data.object.metadata?.workspaceID + const userID = body.data.object.metadata?.userID + const customerID = body.data.object.customer as string + const invoiceID = body.data.object.latest_invoice as string + const subscriptionID = body.data.object.id as string - if (!workspaceID) throw new Error("Workspace ID not found") - if (!customerID) throw new Error("Customer ID not found") - if (!amountInCents) throw new Error("Amount not found") - if (!invoiceID) throw new Error("Invoice ID not found") - if (!subscriptionID) throw new Error("Subscription ID not found") + if (!workspaceID) throw new Error("Workspace ID not found") + if (!userID) throw new Error("User ID not found") + if (!customerID) throw new Error("Customer ID not found") + if (!invoiceID) throw new Error("Invoice ID not found") + if (!subscriptionID) throw new Error("Subscription ID not found") - // get payment id from invoice - const invoice = await Billing.stripe().invoices.retrieve(invoiceID, { - expand: ["payments"], - }) - const paymentID = invoice.payments?.data[0].payment.payment_intent as string - if (!paymentID) throw new Error("Payment ID not found") + // get payment id from invoice + const invoice = await Billing.stripe().invoices.retrieve(invoiceID, { + expand: ["payments"], + }) + const paymentID = invoice.payments?.data[0].payment.payment_intent as string + if (!paymentID) throw new Error("Payment ID not found") - // get payment method for the payment intent - const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, { - expand: ["payment_method"], - }) - const paymentMethod = paymentIntent.payment_method - if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded") + // get payment method for the payment intent + const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, { + expand: ["payment_method"], + }) + const paymentMethod = paymentIntent.payment_method + if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded") - // get coupon id from promotion code - const couponID = await (async () => { - if (!promoCode) return - const coupon = await Billing.stripe().promotionCodes.retrieve(promoCode) - const couponID = coupon.coupon.id - if (!couponID) throw new Error("Coupon not found for promotion code") - return couponID - })() + await Actor.provide("system", { workspaceID }, async () => { + // look up current billing + const billing = await Billing.get() + if (!billing) throw new Error(`Workspace with ID ${workspaceID} not found`) + if (billing.customerID && billing.customerID !== customerID) throw new Error("Customer ID mismatch") - await Actor.provide("system", { workspaceID }, async () => { - // look up current billing - const billing = await Billing.get() - if (!billing) throw new Error(`Workspace with ID ${workspaceID} not found`) - - // Temporarily skip this check because during Black drop, user can checkout - // as a new customer - //if (billing.customerID !== customerID) throw new Error("Customer ID mismatch") - - // Temporarily check the user to apply to. After Black drop, we will allow - // look up the user to apply to - const users = await Database.use((tx) => - tx - .select({ id: UserTable.id, email: AuthTable.subject }) - .from(UserTable) - .innerJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email"))) - .where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))), - ) - const user = users.find((u) => u.email === customerEmail) ?? users[0] - if (!user) { - console.error(`Error: User with email ${customerEmail} not found in workspace ${workspaceID}`) - process.exit(1) - } - - // set customer metadata - if (!billing?.customerID) { - await Billing.stripe().customers.update(customerID, { - metadata: { - workspaceID, - }, - }) - } - - await Database.transaction(async (tx) => { - await tx - .update(BillingTable) - .set({ - customerID, - subscriptionID, - subscription: { - status: "subscribed", - coupon: couponID, - seats: 1, - plan: "200", + // set customer metadata + if (!billing?.customerID) { + await Billing.stripe().customers.update(customerID, { + metadata: { + workspaceID, }, - paymentMethodID: paymentMethod.id, - paymentMethodLast4: paymentMethod.card?.last4 ?? null, - paymentMethodType: paymentMethod.type, }) - .where(eq(BillingTable.workspaceID, workspaceID)) + } - await tx.insert(SubscriptionTable).values({ - workspaceID, - id: Identifier.create("subscription"), - userID: user.id, - }) + await Database.transaction(async (tx) => { + await tx + .update(BillingTable) + .set({ + customerID, + liteSubscriptionID: subscriptionID, + lite: {}, + paymentMethodID: paymentMethod.id, + paymentMethodLast4: paymentMethod.card?.last4 ?? null, + paymentMethodType: paymentMethod.type, + }) + .where(eq(BillingTable.workspaceID, workspaceID)) - await tx.insert(PaymentTable).values({ - workspaceID, - id: Identifier.create("payment"), - amount: centsToMicroCents(amountInCents), - paymentID, - invoiceID, - customerID, - enrichment: { - type: "subscription", - couponID, - }, + await tx.insert(LiteTable).values({ + workspaceID, + id: Identifier.create("lite"), + userID: userID, + }) }) }) - }) - } - if (body.type === "customer.subscription.created") { - /* -{ - id: "evt_1Smq802SrMQ2Fneksse5FMNV", - object: "event", - api_version: "2025-07-30.basil", - created: 1767766916, - data: { - object: { - id: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD", - object: "subscription", - application: null, - application_fee_percent: null, - automatic_tax: { - disabled_reason: null, - enabled: false, - liability: null, - }, - billing_cycle_anchor: 1770445200, - billing_cycle_anchor_config: null, - billing_mode: { - flexible: { - proration_discounts: "included", - }, - type: "flexible", - updated_at: 1770445200, - }, - billing_thresholds: null, - cancel_at: null, - cancel_at_period_end: false, - canceled_at: null, - cancellation_details: { - comment: null, - feedback: null, - reason: null, - }, - collection_method: "charge_automatically", - created: 1770445200, - currency: "usd", - customer: "cus_TkKmZZvysJ2wej", - customer_account: null, - days_until_due: null, - default_payment_method: null, - default_source: "card_1Smq7u2SrMQ2FneknjyOa7sq", - default_tax_rates: [], - description: null, - discounts: [], - ended_at: null, - invoice_settings: { - account_tax_ids: null, - issuer: { - type: "self", - }, - }, - items: { - object: "list", - data: [ - { - id: "si_TkKnBKXFX76t0O", - object: "subscription_item", - billing_thresholds: null, - created: 1770445200, - current_period_end: 1772864400, - current_period_start: 1770445200, - discounts: [], - metadata: {}, - plan: { - id: "price_1SmfFG2SrMQ2FnekJuzwHMea", - object: "plan", - active: true, - amount: 20000, - amount_decimal: "20000", - billing_scheme: "per_unit", - created: 1767725082, - currency: "usd", - interval: "month", - interval_count: 1, - livemode: false, - metadata: {}, - meter: null, - nickname: null, - product: "prod_Tk9LjWT1n0DgYm", - tiers_mode: null, - transform_usage: null, - trial_period_days: null, - usage_type: "licensed", - }, - price: { - id: "price_1SmfFG2SrMQ2FnekJuzwHMea", - object: "price", - active: true, - billing_scheme: "per_unit", - created: 1767725082, - currency: "usd", - custom_unit_amount: null, - livemode: false, - lookup_key: null, - metadata: {}, - nickname: null, - product: "prod_Tk9LjWT1n0DgYm", - recurring: { - interval: "month", - interval_count: 1, - meter: null, - trial_period_days: null, - usage_type: "licensed", - }, - tax_behavior: "unspecified", - tiers_mode: null, - transform_quantity: null, - type: "recurring", - unit_amount: 20000, - unit_amount_decimal: "20000", - }, - quantity: 1, - subscription: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD", - tax_rates: [], - }, - ], - has_more: false, - total_count: 1, - url: "/v1/subscription_items?subscription=sub_1Smq7x2SrMQ2Fnek8F1yf3ZD", - }, - latest_invoice: "in_1Smq7x2SrMQ2FnekSJesfPwE", - livemode: false, - metadata: {}, - next_pending_invoice_item_invoice: null, - on_behalf_of: null, - pause_collection: null, - payment_settings: { - payment_method_options: null, - payment_method_types: null, - save_default_payment_method: "off", - }, - pending_invoice_item_interval: null, - pending_setup_intent: null, - pending_update: null, - plan: { - id: "price_1SmfFG2SrMQ2FnekJuzwHMea", - object: "plan", - active: true, - amount: 20000, - amount_decimal: "20000", - billing_scheme: "per_unit", - created: 1767725082, - currency: "usd", - interval: "month", - interval_count: 1, - livemode: false, - metadata: {}, - meter: null, - nickname: null, - product: "prod_Tk9LjWT1n0DgYm", - tiers_mode: null, - transform_usage: null, - trial_period_days: null, - usage_type: "licensed", - }, - quantity: 1, - schedule: null, - start_date: 1770445200, - status: "active", - test_clock: "clock_1Smq6n2SrMQ2FnekQw4yt2PZ", - transfer_data: null, - trial_end: null, - trial_settings: { - end_behavior: { - missing_payment_method: "create_invoice", - }, - }, - trial_start: null, - }, - }, - livemode: false, - pending_webhooks: 0, - request: { - id: "req_6YO9stvB155WJD", - idempotency_key: "581ba059-6f86-49b2-9c49-0d8450255322", - }, - type: "customer.subscription.created", -} - */ + } } if (body.type === "customer.subscription.updated" && body.data.object.status === "incomplete_expired") { const subscriptionID = body.data.object.id if (!subscriptionID) throw new Error("Subscription ID not found") - await Billing.unsubscribe({ subscriptionID }) + const productID = body.data.object.items.data[0].price.product as string + if (productID === LiteData.productID()) { + await Billing.unsubscribeLite({ subscriptionID }) + } else if (productID === BlackData.productID()) { + await Billing.unsubscribeBlack({ subscriptionID }) + } } if (body.type === "customer.subscription.deleted") { const subscriptionID = body.data.object.id if (!subscriptionID) throw new Error("Subscription ID not found") - await Billing.unsubscribe({ subscriptionID }) + const productID = body.data.object.items.data[0].price.product as string + if (productID === LiteData.productID()) { + await Billing.unsubscribeLite({ subscriptionID }) + } else if (productID === BlackData.productID()) { + await Billing.unsubscribeBlack({ subscriptionID }) + } } if (body.type === "invoice.payment_succeeded") { if ( @@ -430,6 +213,7 @@ export async function POST(input: APIEvent) { typeof subscriptionData.discounts[0] === "string" ? subscriptionData.discounts[0] : subscriptionData.discounts[0]?.coupon?.id + const productID = subscriptionData.items.data[0].price.product as string // get payment id from invoice const invoice = await Billing.stripe().invoices.retrieve(invoiceID, { @@ -459,7 +243,7 @@ export async function POST(input: APIEvent) { invoiceID, customerID, enrichment: { - type: "subscription", + type: productID === LiteData.productID() ? "lite" : "subscription", couponID, }, }), diff --git a/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx index 5326306e2..b8f089864 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx @@ -90,7 +90,7 @@ const enroll = action(async (workspaceID: string) => { "use server" return json( await withActor(async () => { - await Billing.subscribe({ seats: 1 }) + await Billing.subscribeBlack({ seats: 1 }) return { error: undefined } }, workspaceID).catch((e) => ({ error: e.message as string })), { revalidate: [queryBillingInfo.key, querySubscription.key] }, diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx index a252a0234..9fbdad2ef 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx @@ -3,7 +3,8 @@ import { BillingSection } from "./billing-section" import { ReloadSection } from "./reload-section" import { PaymentSection } from "./payment-section" import { BlackSection } from "./black-section" -import { Show } from "solid-js" +import { LiteSection } from "./lite-section" +import { createMemo, Show } from "solid-js" import { createAsync, useParams } from "@solidjs/router" import { queryBillingInfo, querySessionInfo } from "../../common" @@ -11,14 +12,18 @@ export default function () { const params = useParams() const sessionInfo = createAsync(() => querySessionInfo(params.id!)) const billingInfo = createAsync(() => queryBillingInfo(params.id!)) + const isBlack = createMemo(() => billingInfo()?.subscriptionID || billingInfo()?.timeSubscriptionBooked) return (
- + + + + diff --git a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css new file mode 100644 index 000000000..20662ab61 --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css @@ -0,0 +1,160 @@ +.root { + [data-slot="title-row"] { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-4); + } + + [data-slot="usage"] { + display: flex; + gap: var(--space-6); + margin-top: var(--space-4); + + @media (max-width: 40rem) { + flex-direction: column; + gap: var(--space-4); + } + } + + [data-slot="usage-item"] { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--space-2); + } + + [data-slot="usage-header"] { + display: flex; + justify-content: space-between; + align-items: baseline; + } + + [data-slot="usage-label"] { + font-size: var(--font-size-md); + font-weight: 500; + color: var(--color-text); + } + + [data-slot="usage-value"] { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + } + + [data-slot="progress"] { + height: 8px; + background-color: var(--color-bg-surface); + border-radius: var(--border-radius-sm); + overflow: hidden; + } + + [data-slot="progress-bar"] { + height: 100%; + background-color: var(--color-accent); + border-radius: var(--border-radius-sm); + transition: width 0.3s ease; + } + + [data-slot="reset-time"] { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + } + + [data-slot="setting-row"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + margin-top: var(--space-4); + + p { + font-size: var(--font-size-sm); + line-height: 1.5; + color: var(--color-text-secondary); + margin: 0; + } + } + + [data-slot="toggle-label"] { + position: relative; + display: inline-block; + width: 2.5rem; + height: 1.5rem; + cursor: pointer; + flex-shrink: 0; + + input { + opacity: 0; + width: 0; + height: 0; + } + + span { + position: absolute; + inset: 0; + background-color: #ccc; + border: 1px solid #bbb; + border-radius: 1.5rem; + transition: all 0.3s ease; + cursor: pointer; + + &::before { + content: ""; + position: absolute; + top: 50%; + left: 0.125rem; + width: 1.25rem; + height: 1.25rem; + background-color: white; + border: 1px solid #ddd; + border-radius: 50%; + transform: translateY(-50%); + transition: all 0.3s ease; + } + } + + input:checked + span { + background-color: #21ad0e; + border-color: #148605; + + &::before { + transform: translateX(1rem) translateY(-50%); + } + } + + &:hover span { + box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2); + } + + input:checked:hover + span { + box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3); + } + + &:has(input:disabled) { + cursor: not-allowed; + } + + input:disabled + span { + opacity: 0.5; + cursor: not-allowed; + } + } + + [data-slot="other-message"] { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + line-height: 1.5; + } + + [data-slot="promo-description"] { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + line-height: 1.5; + margin-top: var(--space-2); + } + + [data-slot="subscribe-button"] { + align-self: flex-start; + margin-top: var(--space-4); + } +} diff --git a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx new file mode 100644 index 000000000..c9192fdcf --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx @@ -0,0 +1,269 @@ +import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router" +import { createStore } from "solid-js/store" +import { Show } from "solid-js" +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 { Actor } from "@opencode-ai/console-core/actor.js" +import { Subscription } from "@opencode-ai/console-core/subscription.js" +import { LiteData } from "@opencode-ai/console-core/lite.js" +import { withActor } from "~/context/auth.withActor" +import { queryBillingInfo } from "../../common" +import styles from "./lite-section.module.css" +import { useI18n } from "~/context/i18n" + +const queryLiteSubscription = query(async (workspaceID: string) => { + "use server" + return withActor(async () => { + 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, + lite: BillingTable.lite, + }) + .from(BillingTable) + .innerJoin(LiteTable, eq(LiteTable.workspaceID, BillingTable.workspaceID)) + .where(and(eq(LiteTable.workspaceID, Actor.workspace()), isNull(LiteTable.timeDeleted))) + .then((r) => r[0]), + ) + if (!row) return null + + const limits = LiteData.getLimits() + const mine = row.userID === Actor.userID() + + return { + mine, + useBalance: row.lite?.useBalance ?? false, + 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, + }), + } + }, workspaceID) +}, "lite.subscription.get") + +function formatResetTime(seconds: number, i18n: ReturnType) { + const days = Math.floor(seconds / 86400) + if (days >= 1) { + const hours = Math.floor((seconds % 86400) / 3600) + return `${days} ${days === 1 ? i18n.t("workspace.lite.time.day") : i18n.t("workspace.lite.time.days")} ${hours} ${hours === 1 ? i18n.t("workspace.lite.time.hour") : i18n.t("workspace.lite.time.hours")}` + } + const hours = Math.floor(seconds / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + if (hours >= 1) + return `${hours} ${hours === 1 ? i18n.t("workspace.lite.time.hour") : i18n.t("workspace.lite.time.hours")} ${minutes} ${minutes === 1 ? i18n.t("workspace.lite.time.minute") : i18n.t("workspace.lite.time.minutes")}` + if (minutes === 0) return i18n.t("workspace.lite.time.fewSeconds") + return `${minutes} ${minutes === 1 ? i18n.t("workspace.lite.time.minute") : i18n.t("workspace.lite.time.minutes")}` +} + +const createLiteCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => { + "use server" + return json( + await withActor( + () => + Billing.generateLiteCheckoutUrl({ successUrl, cancelUrl }) + .then((data) => ({ error: undefined, data })) + .catch((e) => ({ + error: e.message as string, + data: undefined, + })), + workspaceID, + ), + { revalidate: [queryBillingInfo.key, queryLiteSubscription.key] }, + ) +}, "liteCheckoutUrl") + +const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => { + "use server" + return json( + await withActor( + () => + Billing.generateSessionUrl({ returnUrl }) + .then((data) => ({ error: undefined, data })) + .catch((e) => ({ + error: e.message as string, + data: undefined, + })), + workspaceID, + ), + { revalidate: [queryBillingInfo.key, queryLiteSubscription.key] }, + ) +}, "liteSessionUrl") + +const setLiteUseBalance = action(async (form: FormData) => { + "use server" + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } + const useBalance = form.get("useBalance")?.toString() === "true" + + return json( + await withActor(async () => { + await Database.use((tx) => + tx + .update(BillingTable) + .set({ + lite: useBalance ? { useBalance: true } : {}, + }) + .where(eq(BillingTable.workspaceID, workspaceID)), + ) + return { error: undefined } + }, workspaceID).catch((e) => ({ error: e.message as string })), + { revalidate: [queryBillingInfo.key, queryLiteSubscription.key] }, + ) +}, "setLiteUseBalance") + +export function LiteSection() { + const params = useParams() + const i18n = useI18n() + const lite = createAsync(() => queryLiteSubscription(params.id!)) + const sessionAction = useAction(createSessionUrl) + const sessionSubmission = useSubmission(createSessionUrl) + const checkoutAction = useAction(createLiteCheckoutUrl) + const checkoutSubmission = useSubmission(createLiteCheckoutUrl) + const useBalanceSubmission = useSubmission(setLiteUseBalance) + const [store, setStore] = createStore({ + redirecting: false, + }) + + async function onClickSession() { + const result = await sessionAction(params.id!, window.location.href) + if (result.data) { + setStore("redirecting", true) + window.location.href = result.data + } + } + + async function onClickSubscribe() { + const result = await checkoutAction(params.id!, window.location.href, window.location.href) + if (result.data) { + setStore("redirecting", true) + window.location.href = result.data + } + } + + return ( + <> + + {(sub) => ( +
+
+

{i18n.t("workspace.lite.subscription.title")}

+
+

{i18n.t("workspace.lite.subscription.message")}

+ +
+
+
+
+
+ {i18n.t("workspace.lite.subscription.rollingUsage")} + {sub().rollingUsage.usagePercent}% +
+
+
+
+ + {i18n.t("workspace.lite.subscription.resetsIn")}{" "} + {formatResetTime(sub().rollingUsage.resetInSec, i18n)} + +
+
+
+ {i18n.t("workspace.lite.subscription.weeklyUsage")} + {sub().weeklyUsage.usagePercent}% +
+
+
+
+ + {i18n.t("workspace.lite.subscription.resetsIn")} {formatResetTime(sub().weeklyUsage.resetInSec, i18n)} + +
+
+
+ {i18n.t("workspace.lite.subscription.monthlyUsage")} + {sub().monthlyUsage.usagePercent}% +
+
+
+
+ + {i18n.t("workspace.lite.subscription.resetsIn")}{" "} + {formatResetTime(sub().monthlyUsage.resetInSec, i18n)} + +
+
+
+

{i18n.t("workspace.lite.subscription.useBalance")}

+ + + +
+
+ )} +
+ +
+
+

{i18n.t("workspace.lite.other.title")}

+
+

{i18n.t("workspace.lite.other.message")}

+
+
+ +
+
+

{i18n.t("workspace.lite.promo.title")}

+
+

{i18n.t("workspace.lite.promo.description")}

+ +
+
+ + ) +} diff --git a/packages/console/app/src/routes/workspace/[id]/graph-section.tsx b/packages/console/app/src/routes/workspace/[id]/graph-section.tsx index f26c7291d..56a31cdd0 100644 --- a/packages/console/app/src/routes/workspace/[id]/graph-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/graph-section.tsx @@ -36,7 +36,7 @@ async function getCosts(workspaceID: string, year: number, month: number) { model: UsageTable.model, totalCost: sum(UsageTable.cost), keyId: UsageTable.keyID, - subscription: sql`COALESCE(JSON_EXTRACT(${UsageTable.enrichment}, '$.plan') = 'sub', false)`, + plan: sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`, }) .from(UsageTable) .where( @@ -50,13 +50,13 @@ async function getCosts(workspaceID: string, year: number, month: number) { sql`DATE(${UsageTable.timeCreated})`, UsageTable.model, UsageTable.keyID, - sql`COALESCE(JSON_EXTRACT(${UsageTable.enrichment}, '$.plan') = 'sub', false)`, + sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`, ) .then((x) => x.map((r) => ({ ...r, totalCost: r.totalCost ? parseInt(r.totalCost) : 0, - subscription: Boolean(r.subscription), + plan: r.plan as "sub" | "lite" | "byok" | null, })), ), ) @@ -218,18 +218,21 @@ export function GraphSection() { const colorTextSecondary = styles.getPropertyValue("--color-text-secondary").trim() const colorBorder = styles.getPropertyValue("--color-border").trim() const subSuffix = ` (${i18n.t("workspace.cost.subscriptionShort")})` + const liteSuffix = ` (${i18n.t("workspace.cost.liteShort")})` + const dailyDataRegular = new Map>() const dailyDataSub = new Map>() - const dailyDataNonSub = new Map>() + const dailyDataLite = new Map>() for (const dateKey of dates) { + dailyDataRegular.set(dateKey, new Map()) dailyDataSub.set(dateKey, new Map()) - dailyDataNonSub.set(dateKey, new Map()) + dailyDataLite.set(dateKey, new Map()) } data.usage .filter((row) => (store.key ? row.keyId === store.key : true)) .forEach((row) => { - const targetMap = row.subscription ? dailyDataSub : dailyDataNonSub + const targetMap = row.plan === "sub" ? dailyDataSub : row.plan === "lite" ? dailyDataLite : dailyDataRegular const dayMap = targetMap.get(row.date) if (!dayMap) return dayMap.set(row.model, (dayMap.get(row.model) ?? 0) + row.totalCost) @@ -237,15 +240,15 @@ export function GraphSection() { const filteredModels = store.model === null ? getModels() : [store.model] - // Create datasets: non-subscription first, then subscription (with hatched pattern effect via opacity) + // Create datasets: regular first, then subscription, then lite (with visual distinction via opacity) const datasets = [ ...filteredModels - .filter((model) => dates.some((date) => (dailyDataNonSub.get(date)?.get(model) || 0) > 0)) + .filter((model) => dates.some((date) => (dailyDataRegular.get(date)?.get(model) || 0) > 0)) .map((model) => { const color = getModelColor(model) return { label: model, - data: dates.map((date) => (dailyDataNonSub.get(date)?.get(model) || 0) / 100_000_000), + data: dates.map((date) => (dailyDataRegular.get(date)?.get(model) || 0) / 100_000_000), backgroundColor: color, hoverBackgroundColor: color, borderWidth: 0, @@ -266,6 +269,21 @@ export function GraphSection() { stack: "subscription", } }), + ...filteredModels + .filter((model) => dates.some((date) => (dailyDataLite.get(date)?.get(model) || 0) > 0)) + .map((model) => { + const color = getModelColor(model) + return { + label: `${model}${liteSuffix}`, + data: dates.map((date) => (dailyDataLite.get(date)?.get(model) || 0) / 100_000_000), + backgroundColor: addOpacityToColor(color, 0.35), + hoverBackgroundColor: addOpacityToColor(color, 0.55), + borderWidth: 1, + borderColor: addOpacityToColor(color, 0.7), + borderDash: [4, 2], + stack: "lite", + } + }), ] return { @@ -347,9 +365,18 @@ export function GraphSection() { const meta = chart.getDatasetMeta(i) const label = dataset.label || "" const isSub = label.endsWith(subSuffix) - const model = isSub ? label.slice(0, -subSuffix.length) : label + const isLite = label.endsWith(liteSuffix) + const model = isSub + ? label.slice(0, -subSuffix.length) + : isLite + ? label.slice(0, -liteSuffix.length) + : label const baseColor = getModelColor(model) - const originalColor = isSub ? addOpacityToColor(baseColor, 0.5) : baseColor + const originalColor = isSub + ? addOpacityToColor(baseColor, 0.5) + : isLite + ? addOpacityToColor(baseColor, 0.35) + : baseColor const color = i === legendItem.datasetIndex ? originalColor : addOpacityToColor(baseColor, 0.15) meta.data.forEach((bar: any) => { bar.options.backgroundColor = color @@ -363,9 +390,18 @@ export function GraphSection() { const meta = chart.getDatasetMeta(i) const label = dataset.label || "" const isSub = label.endsWith(subSuffix) - const model = isSub ? label.slice(0, -subSuffix.length) : label + const isLite = label.endsWith(liteSuffix) + const model = isSub + ? label.slice(0, -subSuffix.length) + : isLite + ? label.slice(0, -liteSuffix.length) + : label const baseColor = getModelColor(model) - const color = isSub ? addOpacityToColor(baseColor, 0.5) : baseColor + const color = isSub + ? addOpacityToColor(baseColor, 0.5) + : isLite + ? addOpacityToColor(baseColor, 0.35) + : baseColor meta.data.forEach((bar: any) => { bar.options.backgroundColor = color }) diff --git a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx index 3d6637610..a20a5bf0d 100644 --- a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx @@ -1,6 +1,6 @@ import { Billing } from "@opencode-ai/console-core/billing.js" import { createAsync, query, useParams } from "@solidjs/router" -import { createMemo, For, Show, createEffect, createSignal } from "solid-js" +import { createMemo, For, Show, Switch, Match, createEffect, createSignal } from "solid-js" import { formatDateUTC, formatDateForTable } from "../common" import { withActor } from "~/context/auth.withActor" import { IconChevronLeft, IconChevronRight, IconBreakdown } from "~/component/icon" @@ -175,23 +175,23 @@ export function UsageSection() {
- ${((usage.cost ?? 0) / 100000000).toFixed(4)}} - > - {i18n.t("workspace.usage.byok", { - amount: ((usage.cost ?? 0) / 100000000).toFixed(4), - })} - - } - > - {i18n.t("workspace.usage.subscription", { - amount: ((usage.cost ?? 0) / 100000000).toFixed(4), - })} - + ${((usage.cost ?? 0) / 100000000).toFixed(4)}}> + + {i18n.t("workspace.usage.subscription", { + amount: ((usage.cost ?? 0) / 100000000).toFixed(4), + })} + + + {i18n.t("workspace.usage.lite", { + amount: ((usage.cost ?? 0) / 100000000).toFixed(4), + })} + + + {i18n.t("workspace.usage.byok", { + amount: ((usage.cost ?? 0) / 100000000).toFixed(4), + })} + + {usage.sessionID?.slice(-8) ?? "-"} diff --git a/packages/console/app/src/routes/workspace/common.tsx b/packages/console/app/src/routes/workspace/common.tsx index 5cbd67183..d41793dd9 100644 --- a/packages/console/app/src/routes/workspace/common.tsx +++ b/packages/console/app/src/routes/workspace/common.tsx @@ -115,6 +115,8 @@ export const queryBillingInfo = query(async (workspaceID: string) => { subscriptionPlan: billing.subscriptionPlan, timeSubscriptionBooked: billing.timeSubscriptionBooked, timeSubscriptionSelected: billing.timeSubscriptionSelected, + lite: billing.lite, + liteSubscriptionID: billing.liteSubscriptionID, } }, workspaceID) }, "billing.get") diff --git a/packages/console/app/src/routes/zen/lite/v1/messages.ts b/packages/console/app/src/routes/zen/lite/v1/messages.ts new file mode 100644 index 000000000..ee401e6aa --- /dev/null +++ b/packages/console/app/src/routes/zen/lite/v1/messages.ts @@ -0,0 +1,12 @@ +import type { APIEvent } from "@solidjs/start/server" +import { handler } from "~/routes/zen/util/handler" + +export function POST(input: APIEvent) { + return handler(input, { + format: "anthropic", + modelList: "lite", + parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined, + parseModel: (url: string, body: any) => body.model, + parseIsStream: (url: string, body: any) => !!body.stream, + }) +} diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 6096d7378..80a4b3ab7 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -1,9 +1,9 @@ import type { APIEvent } from "@solidjs/start/server" import { and, Database, eq, isNull, lt, or, sql } from "@opencode-ai/console-core/drizzle/index.js" import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" -import { BillingTable, SubscriptionTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js" +import { BillingTable, LiteTable, SubscriptionTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js" import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js" -import { getWeekBounds } from "@opencode-ai/console-core/util/date.js" +import { getMonthlyBounds, getWeekBounds } from "@opencode-ai/console-core/util/date.js" import { Identifier } from "@opencode-ai/console-core/identifier.js" import { Billing } from "@opencode-ai/console-core/billing.js" import { Actor } from "@opencode-ai/console-core/actor.js" @@ -33,13 +33,14 @@ import { createRateLimiter } from "./rateLimiter" import { createDataDumper } from "./dataDumper" import { createTrialLimiter } from "./trialLimiter" import { createStickyTracker } from "./stickyProviderTracker" +import { LiteData } from "@opencode-ai/console-core/lite.js" type ZenData = Awaited> type RetryOptions = { excludeProviders: string[] retryCount: number } -type BillingSource = "anonymous" | "free" | "byok" | "subscription" | "balance" +type BillingSource = "anonymous" | "free" | "byok" | "subscription" | "lite" | "balance" export async function handler( input: APIEvent, @@ -454,6 +455,7 @@ export async function handler( reloadTrigger: BillingTable.reloadTrigger, timeReloadLockedTill: BillingTable.timeReloadLockedTill, subscription: BillingTable.subscription, + lite: BillingTable.lite, }, user: { id: UserTable.id, @@ -461,13 +463,23 @@ export async function handler( monthlyUsage: UserTable.monthlyUsage, timeMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated, }, - subscription: { + black: { id: SubscriptionTable.id, rollingUsage: SubscriptionTable.rollingUsage, fixedUsage: SubscriptionTable.fixedUsage, timeRollingUpdated: SubscriptionTable.timeRollingUpdated, timeFixedUpdated: SubscriptionTable.timeFixedUpdated, }, + lite: { + id: LiteTable.id, + timeCreated: LiteTable.timeCreated, + rollingUsage: LiteTable.rollingUsage, + weeklyUsage: LiteTable.weeklyUsage, + monthlyUsage: LiteTable.monthlyUsage, + timeRollingUpdated: LiteTable.timeRollingUpdated, + timeWeeklyUpdated: LiteTable.timeWeeklyUpdated, + timeMonthlyUpdated: LiteTable.timeMonthlyUpdated, + }, provider: { credentials: ProviderTable.credentials, }, @@ -495,6 +507,14 @@ export async function handler( isNull(SubscriptionTable.timeDeleted), ), ) + .leftJoin( + LiteTable, + and( + eq(LiteTable.workspaceID, KeyTable.workspaceID), + eq(LiteTable.userID, KeyTable.userID), + isNull(LiteTable.timeDeleted), + ), + ) .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted))) .then((rows) => rows[0]), ) @@ -503,8 +523,19 @@ export async function handler( logger.metric({ api_key: data.apiKey, workspace: data.workspaceID, - isSubscription: data.subscription ? true : false, - subscription: data.billing.subscription?.plan, + ...(() => { + if (data.billing.subscription) + return { + isSubscription: true, + subscription: data.billing.subscription.plan, + } + if (data.billing.lite) + return { + isSubscription: true, + subscription: "lite", + } + return {} + })(), }) return { @@ -512,7 +543,8 @@ export async function handler( workspaceID: data.workspaceID, billing: data.billing, user: data.user, - subscription: data.subscription, + black: data.black, + lite: data.lite, provider: data.provider, isFree: FREE_WORKSPACES.includes(data.workspaceID), isDisabled: !!data.timeDisabled, @@ -525,20 +557,20 @@ export async function handler( if (authInfo.isFree) return "free" if (modelInfo.allowAnonymous) return "free" - // Validate subscription billing - if (authInfo.billing.subscription && authInfo.subscription) { - try { - const sub = authInfo.subscription - const plan = authInfo.billing.subscription.plan + const formatRetryTime = (seconds: number) => { + const days = Math.floor(seconds / 86400) + if (days >= 1) return `${days} day${days > 1 ? "s" : ""}` + const hours = Math.floor(seconds / 3600) + const minutes = Math.ceil((seconds % 3600) / 60) + if (hours >= 1) return `${hours}hr ${minutes}min` + return `${minutes}min` + } - const formatRetryTime = (seconds: number) => { - const days = Math.floor(seconds / 86400) - if (days >= 1) return `${days} day${days > 1 ? "s" : ""}` - const hours = Math.floor(seconds / 3600) - const minutes = Math.ceil((seconds % 3600) / 60) - if (hours >= 1) return `${hours}hr ${minutes}min` - return `${minutes}min` - } + // Validate black subscription billing + if (authInfo.billing.subscription && authInfo.black) { + try { + const sub = authInfo.black + const plan = authInfo.billing.subscription.plan // Check weekly limit if (sub.fixedUsage && sub.timeFixedUpdated) { @@ -577,6 +609,62 @@ export async function handler( } } + // Validate lite subscription billing + if (opts.modelList === "lite" && authInfo.billing.lite && authInfo.lite) { + try { + const sub = authInfo.lite + const liteData = LiteData.getLimits() + + // Check weekly limit + if (sub.weeklyUsage && sub.timeWeeklyUpdated) { + const result = Subscription.analyzeWeeklyUsage({ + limit: liteData.weeklyLimit, + usage: sub.weeklyUsage, + timeUpdated: sub.timeWeeklyUpdated, + }) + if (result.status === "rate-limited") + throw new SubscriptionUsageLimitError( + `Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`, + result.resetInSec, + ) + } + + // Check monthly limit + if (sub.monthlyUsage && sub.timeMonthlyUpdated) { + const result = Subscription.analyzeMonthlyUsage({ + limit: liteData.monthlyLimit, + usage: sub.monthlyUsage, + timeUpdated: sub.timeMonthlyUpdated, + timeSubscribed: sub.timeCreated, + }) + if (result.status === "rate-limited") + throw new SubscriptionUsageLimitError( + `Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`, + result.resetInSec, + ) + } + + // Check rolling limit + if (sub.monthlyUsage && sub.timeMonthlyUpdated) { + const result = Subscription.analyzeRollingUsage({ + limit: liteData.rollingLimit, + window: liteData.rollingWindow, + usage: sub.monthlyUsage, + timeUpdated: sub.timeMonthlyUpdated, + }) + if (result.status === "rate-limited") + throw new SubscriptionUsageLimitError( + `Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`, + result.resetInSec, + ) + } + + return "lite" + } catch (e) { + if (!authInfo.billing.lite.useBalance) throw e + } + } + // Validate pay as you go billing const billing = authInfo.billing if (!billing.paymentMethodID) @@ -743,6 +831,7 @@ export async function handler( enrichment: (() => { if (billingSource === "subscription") return { plan: "sub" } if (billingSource === "byok") return { plan: "byok" } + if (billingSource === "lite") return { plan: "lite" } return undefined })(), }), @@ -750,74 +839,115 @@ export async function handler( .update(KeyTable) .set({ timeUsed: sql`now()` }) .where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))), - ...(billingSource === "subscription" - ? (() => { - const plan = authInfo.billing.subscription!.plan - const black = BlackData.getLimits({ plan }) - const week = getWeekBounds(new Date()) - const rollingWindowSeconds = black.rollingWindow * 3600 - return [ - db - .update(SubscriptionTable) - .set({ - fixedUsage: sql` + ...(() => { + if (billingSource === "subscription") { + const plan = authInfo.billing.subscription!.plan + const black = BlackData.getLimits({ plan }) + const week = getWeekBounds(new Date()) + const rollingWindowSeconds = black.rollingWindow * 3600 + return [ + db + .update(SubscriptionTable) + .set({ + fixedUsage: sql` CASE WHEN ${SubscriptionTable.timeFixedUpdated} >= ${week.start} THEN ${SubscriptionTable.fixedUsage} + ${cost} ELSE ${cost} END `, - timeFixedUpdated: sql`now()`, - rollingUsage: sql` + timeFixedUpdated: sql`now()`, + rollingUsage: sql` CASE WHEN UNIX_TIMESTAMP(${SubscriptionTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${SubscriptionTable.rollingUsage} + ${cost} ELSE ${cost} END `, - timeRollingUpdated: sql` + timeRollingUpdated: sql` CASE WHEN UNIX_TIMESTAMP(${SubscriptionTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${SubscriptionTable.timeRollingUpdated} ELSE now() END `, - }) - .where( - and( - eq(SubscriptionTable.workspaceID, authInfo.workspaceID), - eq(SubscriptionTable.userID, authInfo.user.id), - ), + }) + .where( + and( + eq(SubscriptionTable.workspaceID, authInfo.workspaceID), + eq(SubscriptionTable.userID, authInfo.user.id), ), - ] - })() - : [ + ), + ] + } + if (billingSource === "lite") { + const lite = LiteData.getLimits() + const week = getWeekBounds(new Date()) + const month = getMonthlyBounds(new Date(), authInfo.lite!.timeCreated) + const rollingWindowSeconds = lite.rollingWindow * 3600 + return [ db - .update(BillingTable) + .update(LiteTable) .set({ - balance: - billingSource === "free" || billingSource === "byok" - ? sql`${BillingTable.balance} - ${0}` - : sql`${BillingTable.balance} - ${cost}`, monthlyUsage: sql` + CASE + WHEN ${LiteTable.timeMonthlyUpdated} >= ${month.start} THEN ${LiteTable.monthlyUsage} + ${cost} + ELSE ${cost} + END + `, + timeMonthlyUpdated: sql`now()`, + weeklyUsage: sql` + CASE + WHEN ${LiteTable.timeWeeklyUpdated} >= ${week.start} THEN ${LiteTable.weeklyUsage} + ${cost} + ELSE ${cost} + END + `, + timeWeeklyUpdated: sql`now()`, + rollingUsage: sql` + CASE + WHEN UNIX_TIMESTAMP(${LiteTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${LiteTable.rollingUsage} + ${cost} + ELSE ${cost} + END + `, + timeRollingUpdated: sql` + CASE + WHEN UNIX_TIMESTAMP(${LiteTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${LiteTable.timeRollingUpdated} + ELSE now() + END + `, + }) + .where(and(eq(LiteTable.workspaceID, authInfo.workspaceID), eq(LiteTable.userID, authInfo.user.id))), + ] + } + + return [ + db + .update(BillingTable) + .set({ + balance: + billingSource === "free" || billingSource === "byok" + ? sql`${BillingTable.balance} - ${0}` + : sql`${BillingTable.balance} - ${cost}`, + monthlyUsage: sql` CASE WHEN MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${cost} ELSE ${cost} END `, - timeMonthlyUsageUpdated: sql`now()`, - }) - .where(eq(BillingTable.workspaceID, authInfo.workspaceID)), - db - .update(UserTable) - .set({ - monthlyUsage: sql` + timeMonthlyUsageUpdated: sql`now()`, + }) + .where(eq(BillingTable.workspaceID, authInfo.workspaceID)), + db + .update(UserTable) + .set({ + monthlyUsage: sql` CASE WHEN MONTH(${UserTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${UserTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${UserTable.monthlyUsage} + ${cost} ELSE ${cost} END `, - timeMonthlyUsageUpdated: sql`now()`, - }) - .where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id))), - ]), + timeMonthlyUsageUpdated: sql`now()`, + }) + .where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id))), + ] + })(), ]), ) diff --git a/packages/console/core/migrations/20260224043338_nifty_starjammers/migration.sql b/packages/console/core/migrations/20260224043338_nifty_starjammers/migration.sql new file mode 100644 index 000000000..1c97afbd9 --- /dev/null +++ b/packages/console/core/migrations/20260224043338_nifty_starjammers/migration.sql @@ -0,0 +1,19 @@ +CREATE TABLE `lite` ( + `id` varchar(30) NOT NULL, + `workspace_id` varchar(30) NOT NULL, + `time_created` timestamp(3) NOT NULL DEFAULT (now()), + `time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `time_deleted` timestamp(3), + `user_id` varchar(30) NOT NULL, + `rolling_usage` bigint, + `weekly_usage` bigint, + `monthly_usage` bigint, + `time_rolling_updated` timestamp(3), + `time_weekly_updated` timestamp(3), + `time_monthly_updated` timestamp(3), + CONSTRAINT `PRIMARY` PRIMARY KEY(`workspace_id`,`id`), + CONSTRAINT `workspace_user_id` UNIQUE INDEX(`workspace_id`,`user_id`) +); +--> statement-breakpoint +ALTER TABLE `billing` ADD `lite_subscription_id` varchar(28);--> statement-breakpoint +ALTER TABLE `billing` ADD `lite` json; \ No newline at end of file diff --git a/packages/console/core/migrations/20260224043338_nifty_starjammers/snapshot.json b/packages/console/core/migrations/20260224043338_nifty_starjammers/snapshot.json new file mode 100644 index 000000000..703ee233f --- /dev/null +++ b/packages/console/core/migrations/20260224043338_nifty_starjammers/snapshot.json @@ -0,0 +1,2505 @@ +{ + "version": "6", + "dialect": "mysql", + "id": "5e506dec-61e7-4726-81d1-afa4ffbc61ed", + "prevIds": [ + "4bf45b3f-3edd-4db7-94d5-097aa55ca5f7" + ], + "ddl": [ + { + "name": "account", + "entityType": "tables" + }, + { + "name": "auth", + "entityType": "tables" + }, + { + "name": "benchmark", + "entityType": "tables" + }, + { + "name": "billing", + "entityType": "tables" + }, + { + "name": "lite", + "entityType": "tables" + }, + { + "name": "payment", + "entityType": "tables" + }, + { + "name": "subscription", + "entityType": "tables" + }, + { + "name": "usage", + "entityType": "tables" + }, + { + "name": "ip_rate_limit", + "entityType": "tables" + }, + { + "name": "ip", + "entityType": "tables" + }, + { + "name": "key", + "entityType": "tables" + }, + { + "name": "model", + "entityType": "tables" + }, + { + "name": "provider", + "entityType": "tables" + }, + { + "name": "user", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "account" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "auth" + }, + { + "type": "enum('email','github','google')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subject", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "mediumtext", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "result", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(32)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_type", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(4)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_last4", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "balance", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_trigger", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_amount", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_locked_till", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "enum('20','100','200')", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_plan", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_booked", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_selected", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite_subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "weekly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_weekly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "invoice_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "amount", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_refunded", + "entityType": "columns", + "table": "payment" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "fixed_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_fixed_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "input_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "output_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reasoning_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_read_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_5m_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_1h_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(10)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "ip" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "usage", + "entityType": "columns", + "table": "ip" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "provider" + }, + { + "type": "text", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "credentials", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_seen", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "color", + "entityType": "columns", + "table": "user" + }, + { + "type": "enum('admin','member')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "user" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "workspace" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "auth", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "benchmark", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "billing", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "lite", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "payment", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "subscription", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "usage", + "entityType": "pks" + }, + { + "columns": [ + "ip", + "interval" + ], + "name": "PRIMARY", + "table": "ip_rate_limit", + "entityType": "pks" + }, + { + "columns": [ + "ip" + ], + "name": "PRIMARY", + "table": "ip", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "key", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "model", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "provider", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "user", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "provider", + "isExpression": false + }, + { + "value": "subject", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "provider", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "account_id", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "time_created", + "entityType": "indexes", + "table": "benchmark" + }, + { + "columns": [ + { + "value": "customer_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_customer_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "subscription_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_subscription_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "lite" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "subscription" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "usage_time_created", + "entityType": "indexes", + "table": "usage" + }, + { + "columns": [ + { + "value": "key", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_key", + "entityType": "indexes", + "table": "key" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "model", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "model_workspace_model", + "entityType": "indexes", + "table": "model" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "provider", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_provider", + "entityType": "indexes", + "table": "provider" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "email", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "email", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "slug", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "slug", + "entityType": "indexes", + "table": "workspace" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/console/core/script/black-select-workspaces.ts b/packages/console/core/script/black-select-workspaces.ts index f22478e1b..63bfab887 100644 --- a/packages/console/core/script/black-select-workspaces.ts +++ b/packages/console/core/script/black-select-workspaces.ts @@ -1,10 +1,10 @@ import { Database, eq, and, sql, inArray, isNull, count } from "../src/drizzle/index.js" -import { BillingTable, SubscriptionPlan } from "../src/schema/billing.sql.js" +import { BillingTable, BlackPlans } from "../src/schema/billing.sql.js" import { UserTable } from "../src/schema/user.sql.js" import { AuthTable } from "../src/schema/auth.sql.js" -const plan = process.argv[2] as (typeof SubscriptionPlan)[number] -if (!SubscriptionPlan.includes(plan)) { +const plan = process.argv[2] as (typeof BlackPlans)[number] +if (!BlackPlans.includes(plan)) { console.error("Usage: bun foo.ts ") process.exit(1) } diff --git a/packages/console/core/script/lookup-user.ts b/packages/console/core/script/lookup-user.ts index 6367fd89a..0dfda2411 100644 --- a/packages/console/core/script/lookup-user.ts +++ b/packages/console/core/script/lookup-user.ts @@ -1,13 +1,7 @@ import { Database, and, eq, sql } from "../src/drizzle/index.js" import { AuthTable } from "../src/schema/auth.sql.js" import { UserTable } from "../src/schema/user.sql.js" -import { - BillingTable, - PaymentTable, - SubscriptionTable, - SubscriptionPlan, - UsageTable, -} from "../src/schema/billing.sql.js" +import { BillingTable, PaymentTable, SubscriptionTable, BlackPlans, UsageTable } from "../src/schema/billing.sql.js" import { WorkspaceTable } from "../src/schema/workspace.sql.js" import { BlackData } from "../src/black.js" import { centsToMicroCents } from "../src/util/price.js" @@ -235,7 +229,7 @@ function formatRetryTime(seconds: number) { function getSubscriptionStatus(row: { subscription: { - plan: (typeof SubscriptionPlan)[number] + plan: (typeof BlackPlans)[number] } | null timeSubscriptionCreated: Date | null fixedUsage: number | null diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index 2c1cdb068..fcf238a35 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -1,6 +1,6 @@ import { Stripe } from "stripe" import { Database, eq, sql } from "./drizzle" -import { BillingTable, PaymentTable, SubscriptionTable, UsageTable } from "./schema/billing.sql" +import { BillingTable, LiteTable, PaymentTable, SubscriptionTable, UsageTable } from "./schema/billing.sql" import { Actor } from "./actor" import { fn } from "./util/fn" import { z } from "zod" @@ -9,6 +9,7 @@ import { Identifier } from "./identifier" import { centsToMicroCents } from "./util/price" import { User } from "./user" import { BlackData } from "./black" +import { LiteData } from "./lite" export namespace Billing { export const ITEM_CREDIT_NAME = "opencode credits" @@ -233,6 +234,56 @@ export namespace Billing { }, ) + export const generateLiteCheckoutUrl = fn( + z.object({ + successUrl: z.string(), + cancelUrl: z.string(), + }), + async (input) => { + const user = Actor.assert("user") + const { successUrl, cancelUrl } = input + + const email = await User.getAuthEmail(user.properties.userID) + const billing = await Billing.get() + + if (billing.subscriptionID) throw new Error("Already subscribed to Black") + if (billing.liteSubscriptionID) throw new Error("Already subscribed to Lite") + + const session = await Billing.stripe().checkout.sessions.create({ + mode: "subscription", + billing_address_collection: "required", + line_items: [{ price: LiteData.priceID(), quantity: 1 }], + ...(billing.customerID + ? { + customer: billing.customerID, + customer_update: { + name: "auto", + address: "auto", + }, + } + : { + customer_email: email!, + }), + currency: "usd", + payment_method_types: ["card"], + tax_id_collection: { + enabled: true, + }, + success_url: successUrl, + cancel_url: cancelUrl, + subscription_data: { + metadata: { + workspaceID: Actor.workspace(), + userID: user.properties.userID, + type: "lite", + }, + }, + }) + + return session.url + }, + ) + export const generateSessionUrl = fn( z.object({ returnUrl: z.string(), @@ -271,7 +322,7 @@ export namespace Billing { }, ) - export const subscribe = fn( + export const subscribeBlack = fn( z.object({ seats: z.number(), coupon: z.string().optional(), @@ -336,7 +387,7 @@ export namespace Billing { }, ) - export const unsubscribe = fn( + export const unsubscribeBlack = fn( z.object({ subscriptionID: z.string(), }), @@ -360,4 +411,29 @@ export namespace Billing { }) }, ) + + export const unsubscribeLite = fn( + z.object({ + subscriptionID: z.string(), + }), + async ({ subscriptionID }) => { + const workspaceID = await Database.use((tx) => + tx + .select({ workspaceID: BillingTable.workspaceID }) + .from(BillingTable) + .where(eq(BillingTable.liteSubscriptionID, subscriptionID)) + .then((rows) => rows[0]?.workspaceID), + ) + if (!workspaceID) throw new Error("Workspace ID not found for subscription") + + await Database.transaction(async (tx) => { + await tx + .update(BillingTable) + .set({ liteSubscriptionID: null, lite: null }) + .where(eq(BillingTable.workspaceID, workspaceID)) + + await tx.delete(LiteTable).where(eq(LiteTable.workspaceID, workspaceID)) + }) + }, + ) } diff --git a/packages/console/core/src/black.ts b/packages/console/core/src/black.ts index b4cc27064..a18c5258d 100644 --- a/packages/console/core/src/black.ts +++ b/packages/console/core/src/black.ts @@ -1,7 +1,7 @@ import { z } from "zod" import { fn } from "./util/fn" import { Resource } from "@opencode-ai/console-resource" -import { SubscriptionPlan } from "./schema/billing.sql" +import { BlackPlans } from "./schema/billing.sql" export namespace BlackData { const Schema = z.object({ @@ -28,7 +28,7 @@ export namespace BlackData { export const getLimits = fn( z.object({ - plan: z.enum(SubscriptionPlan), + plan: z.enum(BlackPlans), }), ({ plan }) => { const json = JSON.parse(Resource.ZEN_BLACK_LIMITS.value) @@ -36,9 +36,11 @@ export namespace BlackData { }, ) + export const productID = fn(z.void(), () => Resource.ZEN_BLACK_PRICE.product) + export const planToPriceID = fn( z.object({ - plan: z.enum(SubscriptionPlan), + plan: z.enum(BlackPlans), }), ({ plan }) => { if (plan === "200") return Resource.ZEN_BLACK_PRICE.plan200 diff --git a/packages/console/core/src/identifier.ts b/packages/console/core/src/identifier.ts index b10bf32f6..8aa324ba0 100644 --- a/packages/console/core/src/identifier.ts +++ b/packages/console/core/src/identifier.ts @@ -8,6 +8,7 @@ export namespace Identifier { benchmark: "ben", billing: "bil", key: "key", + lite: "lit", model: "mod", payment: "pay", provider: "prv", diff --git a/packages/console/core/src/lite.ts b/packages/console/core/src/lite.ts index d6679208d..49d23e59e 100644 --- a/packages/console/core/src/lite.ts +++ b/packages/console/core/src/lite.ts @@ -4,9 +4,10 @@ import { Resource } from "@opencode-ai/console-resource" export namespace LiteData { const Schema = z.object({ - fixedLimit: z.number().int(), rollingLimit: z.number().int(), rollingWindow: z.number().int(), + weeklyLimit: z.number().int(), + monthlyLimit: z.number().int(), }) export const validate = fn(Schema, (input) => { @@ -18,11 +19,7 @@ export namespace LiteData { return Schema.parse(json) }) - export const planToPriceID = fn(z.void(), () => { - return Resource.ZEN_LITE_PRICE.price - }) - - export const priceIDToPlan = fn(z.void(), () => { - return "lite" - }) + export const productID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.product) + export const priceID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.price) + export const planName = fn(z.void(), () => "lite") } diff --git a/packages/console/core/src/schema/billing.sql.ts b/packages/console/core/src/schema/billing.sql.ts index 6d96fc7eb..a5c70c211 100644 --- a/packages/console/core/src/schema/billing.sql.ts +++ b/packages/console/core/src/schema/billing.sql.ts @@ -2,7 +2,7 @@ import { bigint, boolean, index, int, json, mysqlEnum, mysqlTable, uniqueIndex, import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types" import { workspaceIndexes } from "./workspace.sql" -export const SubscriptionPlan = ["20", "100", "200"] as const +export const BlackPlans = ["20", "100", "200"] as const export const BillingTable = mysqlTable( "billing", { @@ -25,14 +25,18 @@ export const BillingTable = mysqlTable( subscription: json("subscription").$type<{ status: "subscribed" seats: number - plan: "20" | "100" | "200" + plan: (typeof BlackPlans)[number] useBalance?: boolean coupon?: string }>(), subscriptionID: varchar("subscription_id", { length: 28 }), - subscriptionPlan: mysqlEnum("subscription_plan", SubscriptionPlan), + subscriptionPlan: mysqlEnum("subscription_plan", BlackPlans), timeSubscriptionBooked: utc("time_subscription_booked"), timeSubscriptionSelected: utc("time_subscription_selected"), + liteSubscriptionID: varchar("lite_subscription_id", { length: 28 }), + lite: json("lite").$type<{ + useBalance?: boolean + }>(), }, (table) => [ ...workspaceIndexes(table), @@ -55,6 +59,22 @@ export const SubscriptionTable = mysqlTable( (table) => [...workspaceIndexes(table), uniqueIndex("workspace_user_id").on(table.workspaceID, table.userID)], ) +export const LiteTable = mysqlTable( + "lite", + { + ...workspaceColumns, + ...timestamps, + userID: ulid("user_id").notNull(), + rollingUsage: bigint("rolling_usage", { mode: "number" }), + weeklyUsage: bigint("weekly_usage", { mode: "number" }), + monthlyUsage: bigint("monthly_usage", { mode: "number" }), + timeRollingUpdated: utc("time_rolling_updated"), + timeWeeklyUpdated: utc("time_weekly_updated"), + timeMonthlyUpdated: utc("time_monthly_updated"), + }, + (table) => [...workspaceIndexes(table), uniqueIndex("workspace_user_id").on(table.workspaceID, table.userID)], +) + export const PaymentTable = mysqlTable( "payment", { diff --git a/packages/console/core/src/subscription.ts b/packages/console/core/src/subscription.ts index ca3b17042..879f940e0 100644 --- a/packages/console/core/src/subscription.ts +++ b/packages/console/core/src/subscription.ts @@ -1,7 +1,7 @@ import { z } from "zod" import { fn } from "./util/fn" import { centsToMicroCents } from "./util/price" -import { getWeekBounds } from "./util/date" +import { getWeekBounds, getMonthlyBounds } from "./util/date" export namespace Subscription { export const analyzeRollingUsage = fn( @@ -29,7 +29,7 @@ export namespace Subscription { return { status: "ok" as const, resetInSec: Math.ceil((windowEnd.getTime() - now.getTime()) / 1000), - usagePercent: Math.ceil(Math.min(100, (usage / rollingLimitInMicroCents) * 100)), + usagePercent: Math.floor(Math.min(100, (usage / rollingLimitInMicroCents) * 100)), } } return { @@ -61,7 +61,7 @@ export namespace Subscription { return { status: "ok" as const, resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000), - usagePercent: Math.ceil(Math.min(100, (usage / fixedLimitInMicroCents) * 100)), + usagePercent: Math.floor(Math.min(100, (usage / fixedLimitInMicroCents) * 100)), } } @@ -72,4 +72,38 @@ export namespace Subscription { } }, ) + + export const analyzeMonthlyUsage = fn( + z.object({ + limit: z.number().int(), + usage: z.number().int(), + timeUpdated: z.date(), + timeSubscribed: z.date(), + }), + ({ limit, usage, timeUpdated, timeSubscribed }) => { + const now = new Date() + const month = getMonthlyBounds(now, timeSubscribed) + const fixedLimitInMicroCents = centsToMicroCents(limit * 100) + if (timeUpdated < month.start) { + return { + status: "ok" as const, + resetInSec: Math.ceil((month.end.getTime() - now.getTime()) / 1000), + usagePercent: 0, + } + } + if (usage < fixedLimitInMicroCents) { + return { + status: "ok" as const, + resetInSec: Math.ceil((month.end.getTime() - now.getTime()) / 1000), + usagePercent: Math.floor(Math.min(100, (usage / fixedLimitInMicroCents) * 100)), + } + } + + return { + status: "rate-limited" as const, + resetInSec: Math.ceil((month.end.getTime() - now.getTime()) / 1000), + usagePercent: 100, + } + }, + ) } diff --git a/packages/console/core/src/util/date.test.ts b/packages/console/core/src/util/date.test.ts deleted file mode 100644 index 074df8a2f..000000000 --- a/packages/console/core/src/util/date.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { getWeekBounds } from "./date" - -describe("util.date.getWeekBounds", () => { - test("returns a Monday-based week for Sunday dates", () => { - const date = new Date("2026-01-18T12:00:00Z") - const bounds = getWeekBounds(date) - - expect(bounds.start.toISOString()).toBe("2026-01-12T00:00:00.000Z") - expect(bounds.end.toISOString()).toBe("2026-01-19T00:00:00.000Z") - }) - - test("returns a seven day window", () => { - const date = new Date("2026-01-14T12:00:00Z") - const bounds = getWeekBounds(date) - - const span = bounds.end.getTime() - bounds.start.getTime() - expect(span).toBe(7 * 24 * 60 * 60 * 1000) - }) -}) diff --git a/packages/console/core/src/util/date.ts b/packages/console/core/src/util/date.ts index 9c1ab12d2..dea9c390e 100644 --- a/packages/console/core/src/util/date.ts +++ b/packages/console/core/src/util/date.ts @@ -7,3 +7,32 @@ export function getWeekBounds(date: Date) { end.setUTCDate(start.getUTCDate() + 7) return { start, end } } + +export function getMonthlyBounds(now: Date, subscribed: Date) { + const day = subscribed.getUTCDate() + const hh = subscribed.getUTCHours() + const mm = subscribed.getUTCMinutes() + const ss = subscribed.getUTCSeconds() + const ms = subscribed.getUTCMilliseconds() + + function anchor(year: number, month: number) { + const max = new Date(Date.UTC(year, month + 1, 0)).getUTCDate() + return new Date(Date.UTC(year, month, Math.min(day, max), hh, mm, ss, ms)) + } + + function shift(year: number, month: number, delta: number) { + const total = year * 12 + month + delta + return [Math.floor(total / 12), ((total % 12) + 12) % 12] as const + } + + let y = now.getUTCFullYear() + let m = now.getUTCMonth() + let start = anchor(y, m) + if (start > now) { + ;[y, m] = shift(y, m, -1) + start = anchor(y, m) + } + const [ny, nm] = shift(y, m, 1) + const end = anchor(ny, nm) + return { start, end } +} diff --git a/packages/console/core/test/date.test.ts b/packages/console/core/test/date.test.ts new file mode 100644 index 000000000..e5a0a90e5 --- /dev/null +++ b/packages/console/core/test/date.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, test } from "bun:test" +import { getWeekBounds, getMonthlyBounds } from "../src/util/date" + +describe("util.date.getWeekBounds", () => { + test("returns a Monday-based week for Sunday dates", () => { + const date = new Date("2026-01-18T12:00:00Z") + const bounds = getWeekBounds(date) + + expect(bounds.start.toISOString()).toBe("2026-01-12T00:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-01-19T00:00:00.000Z") + }) + + test("returns a seven day window", () => { + const date = new Date("2026-01-14T12:00:00Z") + const bounds = getWeekBounds(date) + + const span = bounds.end.getTime() - bounds.start.getTime() + expect(span).toBe(7 * 24 * 60 * 60 * 1000) + }) +}) + +describe("util.date.getMonthlyBounds", () => { + test("resets on subscription day mid-month", () => { + const now = new Date("2026-03-20T10:00:00Z") + const subscribed = new Date("2026-01-15T08:00:00Z") + const bounds = getMonthlyBounds(now, subscribed) + + expect(bounds.start.toISOString()).toBe("2026-03-15T08:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-04-15T08:00:00.000Z") + }) + + test("before subscription day in current month uses previous month anchor", () => { + const now = new Date("2026-03-10T10:00:00Z") + const subscribed = new Date("2026-01-15T08:00:00Z") + const bounds = getMonthlyBounds(now, subscribed) + + expect(bounds.start.toISOString()).toBe("2026-02-15T08:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-03-15T08:00:00.000Z") + }) + + test("clamps day for short months", () => { + const now = new Date("2026-03-01T10:00:00Z") + const subscribed = new Date("2026-01-31T12:00:00Z") + const bounds = getMonthlyBounds(now, subscribed) + + expect(bounds.start.toISOString()).toBe("2026-02-28T12:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-03-31T12:00:00.000Z") + }) + + test("handles subscription on the 1st", () => { + const now = new Date("2026-04-15T00:00:00Z") + const subscribed = new Date("2026-01-01T00:00:00Z") + const bounds = getMonthlyBounds(now, subscribed) + + expect(bounds.start.toISOString()).toBe("2026-04-01T00:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-05-01T00:00:00.000Z") + }) + + test("exactly on the reset boundary uses current period", () => { + const now = new Date("2026-03-15T08:00:00Z") + const subscribed = new Date("2026-01-15T08:00:00Z") + const bounds = getMonthlyBounds(now, subscribed) + + expect(bounds.start.toISOString()).toBe("2026-03-15T08:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-04-15T08:00:00.000Z") + }) + + test("february to march with day 30 subscription", () => { + const now = new Date("2026-02-15T06:00:00Z") + const subscribed = new Date("2025-12-30T06:00:00Z") + const bounds = getMonthlyBounds(now, subscribed) + + expect(bounds.start.toISOString()).toBe("2026-01-30T06:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-02-28T06:00:00.000Z") + }) +}) diff --git a/packages/console/core/test/subscription.test.ts b/packages/console/core/test/subscription.test.ts new file mode 100644 index 000000000..57e63f94c --- /dev/null +++ b/packages/console/core/test/subscription.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, test, setSystemTime, afterEach } from "bun:test" +import { Subscription } from "../src/subscription" +import { centsToMicroCents } from "../src/util/price" + +afterEach(() => { + setSystemTime() +}) + +describe("Subscription.analyzeMonthlyUsage", () => { + const subscribed = new Date("2026-01-15T08:00:00Z") + + test("returns ok with 0% when usage was last updated before current period", () => { + setSystemTime(new Date("2026-03-20T10:00:00Z")) + const result = Subscription.analyzeMonthlyUsage({ + limit: 10, + usage: centsToMicroCents(500), + timeUpdated: new Date("2026-02-10T00:00:00Z"), + timeSubscribed: subscribed, + }) + + expect(result.status).toBe("ok") + expect(result.usagePercent).toBe(0) + // reset should be seconds until 2026-04-15T08:00:00Z + const expected = Math.ceil( + (new Date("2026-04-15T08:00:00Z").getTime() - new Date("2026-03-20T10:00:00Z").getTime()) / 1000, + ) + expect(result.resetInSec).toBe(expected) + }) + + test("returns ok with usage percent when under limit", () => { + setSystemTime(new Date("2026-03-20T10:00:00Z")) + const limit = 10 // $10 + const half = centsToMicroCents(10 * 100) / 2 + const result = Subscription.analyzeMonthlyUsage({ + limit, + usage: half, + timeUpdated: new Date("2026-03-18T00:00:00Z"), + timeSubscribed: subscribed, + }) + + expect(result.status).toBe("ok") + expect(result.usagePercent).toBe(50) + }) + + test("returns rate-limited when at or over limit", () => { + setSystemTime(new Date("2026-03-20T10:00:00Z")) + const limit = 10 + const result = Subscription.analyzeMonthlyUsage({ + limit, + usage: centsToMicroCents(limit * 100), + timeUpdated: new Date("2026-03-18T00:00:00Z"), + timeSubscribed: subscribed, + }) + + expect(result.status).toBe("rate-limited") + expect(result.usagePercent).toBe(100) + }) + + test("resets usage when crossing monthly boundary", () => { + // subscribed on 15th, now is April 16th — period is Apr 15 to May 15 + // timeUpdated is March 20 (previous period) + setSystemTime(new Date("2026-04-16T10:00:00Z")) + const result = Subscription.analyzeMonthlyUsage({ + limit: 10, + usage: centsToMicroCents(10 * 100), + timeUpdated: new Date("2026-03-20T00:00:00Z"), + timeSubscribed: subscribed, + }) + + expect(result.status).toBe("ok") + expect(result.usagePercent).toBe(0) + }) + + test("caps usage percent at 100", () => { + setSystemTime(new Date("2026-03-20T10:00:00Z")) + const limit = 10 + const result = Subscription.analyzeMonthlyUsage({ + limit, + usage: centsToMicroCents(limit * 100) - 1, + timeUpdated: new Date("2026-03-18T00:00:00Z"), + timeSubscribed: subscribed, + }) + + expect(result.status).toBe("ok") + expect(result.usagePercent).toBeLessThanOrEqual(100) + }) + + test("handles subscription day 31 in short month", () => { + const sub31 = new Date("2026-01-31T12:00:00Z") + // now is March 1 — period should be Feb 28 to Mar 31 + setSystemTime(new Date("2026-03-01T10:00:00Z")) + const result = Subscription.analyzeMonthlyUsage({ + limit: 10, + usage: 0, + timeUpdated: new Date("2026-03-01T09:00:00Z"), + timeSubscribed: sub31, + }) + + expect(result.status).toBe("ok") + expect(result.usagePercent).toBe(0) + const expected = Math.ceil( + (new Date("2026-03-31T12:00:00Z").getTime() - new Date("2026-03-01T10:00:00Z").getTime()) / 1000, + ) + expect(result.resetInSec).toBe(expected) + }) +})