summaryrefslogtreecommitdiffhomepage
path: root/packages/console/app/src/routes/zen
diff options
context:
space:
mode:
authorFrank <[email protected]>2026-02-24 04:45:39 -0500
committerFrank <[email protected]>2026-02-24 04:45:41 -0500
commitfb6d201ee03d73967c554394742be360e2ff782d (patch)
tree25cbda2e9ec598dfa5a5493f67cb432c5287d624 /packages/console/app/src/routes/zen
parentcda2af2589ddef9265ca2db379ecd4ab556f6be8 (diff)
downloadopencode-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.ts12
-rw-r--r--packages/console/app/src/routes/zen/util/handler.ts246
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))),
+ ]
+ })(),
]),
)