summaryrefslogtreecommitdiffhomepage
path: root/packages/console
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-10-30 14:12:27 -0400
committerFrank <[email protected]>2025-10-30 15:10:29 -0400
commit4f02d7d424bef3d5be7a9bb7a592d4f6d326a8a3 (patch)
tree17c7deb087726854d001e02280526f771586fb5f /packages/console
parent4cebd69bf03fb9f00e2f793eb483251ecbd9a45a (diff)
downloadopencode-4f02d7d424bef3d5be7a9bb7a592d4f6d326a8a3.tar.gz
opencode-4f02d7d424bef3d5be7a9bb7a592d4f6d326a8a3.zip
zen: allow byok requests w/o a balance
Diffstat (limited to 'packages/console')
-rw-r--r--packages/console/app/src/routes/zen/util/handler.ts84
1 files changed, 66 insertions, 18 deletions
diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts
index f7a1f0e16..3163de346 100644
--- a/packages/console/app/src/routes/zen/util/handler.ts
+++ b/packages/console/app/src/routes/zen/util/handler.ts
@@ -13,7 +13,11 @@ import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
import { logger } from "./logger"
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError } from "./error"
-import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider"
+import {
+ createBodyConverter,
+ createStreamPartConverter,
+ createResponseConverter,
+} from "./provider/provider"
import { Format } from "./format"
import { anthropicHelper } from "./provider/anthropic"
import { openaiHelper } from "./provider/openai"
@@ -43,7 +47,11 @@ export async function handler(
})
const zenData = ZenData.list()
const modelInfo = validateModel(zenData, body.model)
- const providerInfo = selectProvider(zenData, modelInfo, input.request.headers.get("x-real-ip") ?? "")
+ const providerInfo = selectProvider(
+ zenData,
+ modelInfo,
+ input.request.headers.get("x-real-ip") ?? "",
+ )
const authInfo = await authenticate(modelInfo, providerInfo)
validateBilling(modelInfo, authInfo)
validateModelSettings(authInfo)
@@ -222,7 +230,11 @@ export async function handler(
return { id: modelId, ...modelData }
}
- function selectProvider(zenData: ZenData, model: Awaited<ReturnType<typeof validateModel>>, ip: string) {
+ function selectProvider(
+ zenData: ZenData,
+ model: Awaited<ReturnType<typeof validateModel>>,
+ ip: string,
+ ) {
const providers = model.providers
.filter((provider) => !provider.disabled)
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
@@ -239,7 +251,11 @@ export async function handler(
return {
...provider,
...zenData.providers[provider.id],
- ...(provider.id === "anthropic" ? anthropicHelper : provider.id === "openai" ? openaiHelper : oaCompatHelper),
+ ...(provider.id === "anthropic"
+ ? anthropicHelper
+ : provider.id === "openai"
+ ? openaiHelper
+ : oaCompatHelper),
}
}
@@ -279,11 +295,20 @@ export async function handler(
.from(KeyTable)
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
.innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID))
- .innerJoin(UserTable, and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID)))
- .leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, model.id)))
+ .innerJoin(
+ UserTable,
+ and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID)),
+ )
+ .leftJoin(
+ ModelTable,
+ and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, model.id)),
+ )
.leftJoin(
ProviderTable,
- and(eq(ProviderTable.workspaceID, KeyTable.workspaceID), eq(ProviderTable.provider, providerInfo.id)),
+ and(
+ eq(ProviderTable.workspaceID, KeyTable.workspaceID),
+ eq(ProviderTable.provider, providerInfo.id),
+ ),
)
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
.then((rows) => rows[0]),
@@ -307,12 +332,20 @@ export async function handler(
}
function validateBilling(model: Model, authInfo: Awaited<ReturnType<typeof authenticate>>) {
- if (!authInfo || authInfo.isFree) return
+ if (!authInfo) return
+ if (authInfo.provider?.credentials) return
+ if (authInfo.isFree) return
if (model.allowAnonymous) return
const billing = authInfo.billing
- if (!billing.paymentMethodID) throw new CreditsError("No payment method")
- if (billing.balance <= 0) throw new CreditsError("Insufficient balance")
+ if (!billing.paymentMethodID)
+ throw new CreditsError(
+ `No payment method. Add a payment method here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
+ )
+ if (billing.balance <= 0)
+ throw new CreditsError(
+ `Insufficient balance. Manage your billing here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
+ )
const now = new Date()
const currentYear = now.getUTCFullYear()
@@ -327,7 +360,7 @@ export async function handler(
const dateMonth = billing.timeMonthlyUsageUpdated.getUTCMonth()
if (currentYear === dateYear && currentMonth === dateMonth)
throw new MonthlyLimitError(
- `Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}.`,
+ `Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
)
}
@@ -340,7 +373,9 @@ export async function handler(
const dateYear = authInfo.user.timeMonthlyUsageUpdated.getUTCFullYear()
const dateMonth = authInfo.user.timeMonthlyUsageUpdated.getUTCMonth()
if (currentYear === dateYear && currentMonth === dateMonth)
- throw new UserLimitError(`You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}.`)
+ throw new UserLimitError(
+ `You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/members`,
+ )
}
}
@@ -364,12 +399,19 @@ export async function handler(
providerInfo: Awaited<ReturnType<typeof selectProvider>>,
usage: any,
) {
- const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
- providerInfo.normalizeUsage(usage)
+ const {
+ inputTokens,
+ outputTokens,
+ reasoningTokens,
+ cacheReadTokens,
+ cacheWrite5mTokens,
+ cacheWrite1hTokens,
+ } = providerInfo.normalizeUsage(usage)
const modelCost =
modelInfo.cost200K &&
- inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) > 200_000
+ inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) >
+ 200_000
? modelInfo.cost200K
: modelInfo.cost
@@ -420,7 +462,8 @@ export async function handler(
if (!authInfo) return
- const cost = authInfo.isFree || authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
+ const cost =
+ authInfo.isFree || authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
await Database.transaction(async (tx) => {
await tx.insert(UsageTable).values({
workspaceID: authInfo.workspaceID,
@@ -460,7 +503,9 @@ export async function handler(
`,
timeMonthlyUsageUpdated: sql`now()`,
})
- .where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id)))
+ .where(
+ and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id)),
+ )
})
await Database.use((tx) =>
@@ -487,7 +532,10 @@ export async function handler(
eq(BillingTable.workspaceID, authInfo.workspaceID),
eq(BillingTable.reload, true),
lt(BillingTable.balance, centsToMicroCents(Billing.CHARGE_THRESHOLD)),
- or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)),
+ or(
+ isNull(BillingTable.timeReloadLockedTill),
+ lt(BillingTable.timeReloadLockedTill, sql`now()`),
+ ),
),
),
)