diff options
| author | Frank <[email protected]> | 2026-02-24 04:45:39 -0500 |
|---|---|---|
| committer | Frank <[email protected]> | 2026-02-24 04:45:41 -0500 |
| commit | fb6d201ee03d73967c554394742be360e2ff782d (patch) | |
| tree | 25cbda2e9ec598dfa5a5493f67cb432c5287d624 /packages/console/app/src/routes/zen | |
| parent | cda2af2589ddef9265ca2db379ecd4ab556f6be8 (diff) | |
| download | opencode-fb6d201ee03d73967c554394742be360e2ff782d.tar.gz opencode-fb6d201ee03d73967c554394742be360e2ff782d.zip | |
wip: zen lite
Diffstat (limited to 'packages/console/app/src/routes/zen')
| -rw-r--r-- | packages/console/app/src/routes/zen/lite/v1/messages.ts | 12 | ||||
| -rw-r--r-- | packages/console/app/src/routes/zen/util/handler.ts | 246 |
2 files changed, 200 insertions, 58 deletions
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<ReturnType<typeof ZenData.list>> 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,21 +557,21 @@ export async function handler( if (authInfo.isFree) return "free" if (modelInfo.allowAnonymous) return "free" - // Validate subscription billing - if (authInfo.billing.subscription && authInfo.subscription) { + 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.subscription + const sub = authInfo.black 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` - } - // Check weekly limit if (sub.fixedUsage && sub.timeFixedUpdated) { const blackData = BlackData.getLimits({ plan }) @@ -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 MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${cost} + WHEN ${LiteTable.timeMonthlyUpdated} >= ${month.start} THEN ${LiteTable.monthlyUsage} + ${cost} ELSE ${cost} END `, - timeMonthlyUsageUpdated: sql`now()`, + 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(eq(BillingTable.workspaceID, authInfo.workspaceID)), - db - .update(UserTable) - .set({ - monthlyUsage: sql` + .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` 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))), + ] + })(), ]), ) |
