1 line
51 KiB
JSON
1 line
51 KiB
JSON
{"repo": "anomalyco/opencode", "files": {"packages/console/app/src/routes/zen/util/handler.ts": {"32f278b48f1a495611165d8a9f1ace0b512933e2": {"t": 1785596367.7319806, "n": 50384, "h": "2649e0e11f7f9f36", "c": "import type { APIEvent } from \"@solidjs/start/server\"\nimport { and, Database, eq, isNull, lt, or, sql } from \"@opencode-ai/console-core/drizzle/index.js\"\nimport { KeyTable } from \"@opencode-ai/console-core/schema/key.sql.js\"\nimport { BillingTable, LiteTable, SubscriptionTable, UsageTable } from \"@opencode-ai/console-core/schema/billing.sql.js\"\nimport { centsToMicroCents } from \"@opencode-ai/console-core/util/price.js\"\nimport { getMonthlyBounds, getWeekBounds } from \"@opencode-ai/console-core/util/date.js\"\nimport { Identifier } from \"@opencode-ai/console-core/identifier.js\"\nimport { Billing } from \"@opencode-ai/console-core/billing.js\"\nimport { Actor } from \"@opencode-ai/console-core/actor.js\"\nimport { WorkspaceTable } from \"@opencode-ai/console-core/schema/workspace.sql.js\"\nimport { ZenData } from \"@opencode-ai/console-core/model.js\"\nimport { Subscription } from \"@opencode-ai/console-core/subscription.js\"\nimport { BlackData } from \"@opencode-ai/console-core/black.js\"\nimport { UserTable } from \"@opencode-ai/console-core/schema/user.sql.js\"\nimport { ModelTable } from \"@opencode-ai/console-core/schema/model.sql.js\"\nimport { ProviderTable } from \"@opencode-ai/console-core/schema/provider.sql.js\"\nimport { logger } from \"./logger\"\nimport {\n AuthError,\n CreditsError,\n MonthlyLimitError,\n UserLimitError,\n ModelError,\n RegionError,\n RateLimitError,\n FreeUsageLimitError,\n GoUsageLimitError,\n BlackUsageLimitError,\n} from \"./error\"\nimport {\n buildCostChunk,\n createBodyConverter,\n createStreamPartConverter,\n createResponseConverter,\n UsageInfo,\n} from \"./provider/provider\"\nimport { anthropicHelper } from \"./provider/anthropic\"\nimport { googleHelper } from \"./provider/google\"\nimport { openaiHelper } from \"./provider/openai\"\nimport { oaCompatHelper } from \"./provider/openai-compatible\"\nimport { createRateLimiter as createIpRateLimiter } from \"./ipRateLimiter\"\nimport { createRateLimiter as createKeyRateLimiter } from \"./keyRateLimiter\"\nimport { createTrialLimiter } from \"./trialLimiter\"\nimport { createStickyTracker } from \"./stickyProviderTracker\"\nimport { LiteData } from \"@opencode-ai/console-core/lite.js\"\nimport { Resource } from \"@opencode-ai/console-resource\"\nimport { i18n, type Key } from \"~/i18n\"\nimport { localeFromRequest } from \"~/lib/language\"\nimport { createModelTpmLimiter } from \"./modelTpmLimiter\"\nimport { createModelTpsLimiter } from \"./modelTpsLimiter\"\nimport { createProviderBudgetTracker } from \"./providerBudgetTracker\"\nimport { accumulateUsage, HOT_WORKSPACES } from \"./usageBatcher\"\nimport { Workspace } from \"@opencode-ai/console-core/workspace.js\"\nimport { countryFromRequest } from \"~/lib/request-country\"\n\ntype ZenData = Awaited<ReturnType<typeof ZenData.list>>\ntype RetryOptions = {\n excludeProviders: string[]\n retryCount: number\n}\ntype BillingSource = \"anonymous\" | \"free\" | \"byok\" | \"subscription\" | \"lite\" | \"balance\"\n\nfunction resolve(text: string, params?: Record<string, string | number>) {\n if (!params) return text\n return text.replace(/\\{\\{(\\w+)\\}\\}/g, (raw, key) => {\n const value = params[key]\n if (value === undefined || value === null) return raw\n return String(value)\n })\n}\n\nexport async function handler(\n input: APIEvent,\n opts: {\n format: ZenData.Format\n modelList: \"lite\" | \"full\"\n parseApiKey: (headers: Headers) => string | undefined\n parseModel: (url: string, body: any) => string\n parseVariant: (url: string, body: any) => string | undefined\n parseIsStream: (url: string, body: any) => boolean\n },\n) {\n type AuthInfo = Awaited<ReturnType<typeof authenticate>>\n type ModelInfo = Awaited<ReturnType<typeof validateModel>>\n type ProviderInfo = Awaited<ReturnType<typeof selectProvider>>\n type CostInfo = ReturnType<typeof calculateCost>\n\n const MAX_FAILOVER_RETRIES = 3\n const MAX_RETRYABLE_STATUS_RETRIES = 3\n const dict = i18n(localeFromRequest(input.request))\n const t = (key: Key, params?: Record<string, string | number>) => resolve(dict[key], params)\n const ADMIN_WORKSPACES = [\n \"wrk_01K46JDFR0E75SG2Q8K172KF3Y\", // anomaly\n \"wrk_01K6W1A3VE0KMNVSCQT43BG2SX\", // benchmark\n \"wrk_01KKZDKDWCS1VTJF8QTX62DD50\", // contributors\n ]\n\n try {\n const url = input.request.url\n const body = await input.request.json()\n const model = opts.parseModel(url, body)\n const variant = opts.parseVariant(url, body)\n const isStream = opts.parseIsStream(url, body)\n const rawIp = input.request.headers.get(\"x-real-ip\") ?? \"\"\n const ip = rawIp.includes(\":\") ? rawIp.split(\":\").slice(0, 4).join(\":\") : rawIp\n const rawZenApiKey = opts.parseApiKey(input.request.headers)\n const zenApiKey = rawZenApiKey === \"public\" ? undefined : rawZenApiKey\n const sessionId = input.request.headers.get(\"x-opencode-session\") ?? \"\"\n const requestId = input.request.headers.get(\"x-opencode-request\") ?? \"\"\n const ocClient = input.request.headers.get(\"x-opencode-client\") ?? \"\"\n const projectId = input.request.headers.get(\"x-opencode-project\") ?? \"\"\n const userAgent = input.request.headers.get(\"user-agent\") ?? \"\"\n logger.metric({\n is_stream: isStream,\n session: sessionId,\n request: requestId,\n client: ocClient,\n user_agent: userAgent,\n \"model.variant\": variant,\n \"model.tier\": opts.modelList === \"full\" ? \"zen\" : \"go\",\n })\n const zenData = ZenData.list(opts.modelList)\n const modelInfo = validateModel(zenData, model)\n const trialLimiter = createTrialLimiter(modelInfo.trialProvider, ip)\n const trialProviders = await trialLimiter?.check()\n const rateLimiter = modelInfo.allowAnonymous\n ? createIpRateLimiter(modelInfo.id, modelInfo.rateLimit, ip, input.request)\n : createKeyRateLimiter(modelInfo.id, modelInfo.rateLimit, zenApiKey, input.request)\n await rateLimiter?.check()\n const authInfo = await authenticate(modelInfo, zenApiKey)\n const allowedRegions = authInfo?.region\n ? authInfo.region\n : await (async () => {\n if (!authInfo) return\n return Actor.provide(\"system\", { workspaceID: authInfo.workspaceID }, () =>\n Workspace.setDefaultRegion({ country: countryFromRequest(input.request) }),\n )\n })()\n if (\n authInfo &&\n opts.modelList === \"lite\" &&\n modelInfo.id === \"deepseek-v4-flash\" &&\n !allowedRegions?.includes(\"cn\")\n )\n throw new RegionError(\n t(\"zen.api.error.regionNotAllowed\", {\n consoleGoUrl: `https://opencode.ai/workspace/${authInfo.workspaceID}/go`,\n }),\n )\n const stickyId = sessionId ? sessionId : (authInfo?.workspaceID ?? ip)\n const stickyTracker = createStickyTracker(modelInfo.id, modelInfo.stickyProvider, stickyId)\n const stickyProvider = await stickyTracker?.get()\n const billingSource = validateBilling(authInfo, modelInfo)\n logger.metric({ source: billingSource })\n const modelTpmLimiter = createModelTpmLimiter(modelInfo.providers)\n const modelTpmLimits = await modelTpmLimiter?.check()\n const modelTpsLimiter = createModelTpsLimiter(modelInfo.providers)\n const modelTpsLimits = await modelTpsLimiter?.check()\n const providerBudgetTracker = createProviderBudgetTracker(\n modelInfo.providers.map((provider) => ({ ...zenData.providers[provider.id], ...provider })),\n )\n const providerBudget = await providerBudgetTracker?.check()\n\n const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {\n const providerInfo = selectProvider(\n model,\n zenData,\n authInfo,\n modelInfo,\n stickyId,\n trialProviders,\n retry,\n stickyProvider,\n modelTpmLimits,\n modelTpsLimits,\n providerBudget,\n )\n validateModelSettings(billingSource, authInfo)\n updateProviderKey(authInfo, providerInfo)\n logger.metric({\n provider: providerInfo.id,\n \"provider.model\": providerInfo.model,\n shallowProvider: providerInfo.id,\n \"shallowProvider.model\": providerInfo.model,\n })\n\n const startTimestamp = Date.now()\n const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream)\n const reqBody = JSON.stringify(\n providerInfo.modifyBody({\n ...createBodyConverter(opts.format, providerInfo.format)(body),\n model: providerInfo.model,\n ...(() => {\n const replacer = (obj: Record<string, any>): Record<string, any> =>\n Object.fromEntries(\n Object.entries(obj).flatMap(([k, v]) => {\n if (Array.isArray(v)) return [[k, v]]\n if (typeof v === \"object\") return [[k, replacer(v)]]\n if (typeof v === \"string\") {\n if (v === \"$workspace\") return authInfo?.workspaceID ? [[k, authInfo?.workspaceID]] : []\n if (v === \"$user\") return stickyId ? [[k, stickyId]] : []\n if (v.startsWith(\"$header.\")) {\n const headerValue = input.request.headers.get(v.slice(8))\n return headerValue ? [[k, headerValue]] : []\n }\n }\n return [[k, v]]\n }),\n )\n return replacer(providerInfo.payloadModifier ?? {})\n })(),\n }),\n )\n logger.debug(\"REQUEST URL: \" + reqUrl)\n logger.debug(\"REQUEST: \" + reqBody.substring(0, 300) + \"...\")\n const isNewInference =\n providerInfo.id.startsWith(\"console.\") ||\n providerInfo.id.startsWith(\"console-go.\") ||\n providerInfo.id.startsWith(\"inf.\") ||\n providerInfo.id.startsWith(\"inf-go.\")\n const res = await fetchWithRetryableStatus(\n reqUrl,\n {\n method: \"POST\",\n headers: (() => {\n const headers = new Headers(input.request.headers)\n providerInfo.modifyHeaders(headers, providerInfo.apiKey, stickyId)\n Object.entries(providerInfo.headerModifier ?? {}).forEach(([k, v]) => {\n if (v === \"$ip\") return headers.set(k, ip)\n if (v === \"$caller\") return headers.set(k, stickyId)\n if (v === \"$session\") return headers.set(k, sessionId)\n if (v === \"$model\") return headers.set(k, model)\n if (v === \"$request\") return headers.set(k, requestId)\n if (v === \"$project\") return headers.set(k, projectId)\n if (v === \"$workspace\") {\n if (authInfo?.workspaceID) headers.set(k, authInfo.workspaceID)\n return\n }\n headers.set(k, v)\n })\n headers.delete(\"host\")\n headers.delete(\"content-length\")\n headers.delete(\"x-opencode-request\")\n headers.delete(\"x-opencode-session\")\n headers.delete(\"x-opencode-project\")\n headers.delete(\"x-opencode-client\")\n return headers\n })(),\n body: reqBody,\n // Propagate caller disconnects to the upstream provider request so\n // abandoned Console requests do not leave orphaned inference work open.\n signal: input.request.signal,\n },\n { count: isNewInference ? MAX_RETRYABLE_STATUS_RETRIES : 0 },\n )\n\n if (isNewInference) {\n const resEndpointId = res.headers.get(\"x-opencode-endpoint-id\")\n const resEndpointModelId = res.headers.get(\"x-opencode-upstream-model-id\")\n if (resEndpointId && resEndpointModelId)\n logger.metric({\n provider: resEndpointId,\n \"provider.model\": resEndpointModelId,\n })\n }\n\n if (res.status !== 200) {\n logger.metric({\n \"llm.error.code\": res.status,\n \"llm.error.message\": res.statusText,\n })\n }\n\n // Try another provider => stop retrying if using fallback provider\n if (\n //!isNewInference &&\n res.status !== 200 &&\n // ie. 400 error is usually provider error like malformed request\n res.status !== 400 &&\n // ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.\n !(modelInfo.id.startsWith(\"gpt-\") && res.status === 404) &&\n // ie. cannot change codex model providers mid-session\n modelInfo.stickyProvider !== \"strict\" &&\n modelInfo.fallbackProvider &&\n providerInfo.id !== modelInfo.fallbackProvider\n ) {\n return retriableRequest({\n excludeProviders: [...retry.excludeProviders, providerInfo.id],\n retryCount: retry.retryCount + 1,\n })\n }\n\n return { providerInfo, reqBody, res, startTimestamp }\n }\n\n const { providerInfo, reqBody, res, startTimestamp } = await retriableRequest()\n\n // Store sticky provider\n if (res.status === 200) await stickyTracker?.set(providerInfo.id)\n\n // Temporarily change 404 to 400 status code b/c solid start automatically override 404 response\n const resStatus = res.status === 404 ? 400 : res.status\n\n // Scrub response headers\n const resHeaders = new Headers()\n const keepHeaders = [\"content-type\", \"cache-control\"]\n for (const [k, v] of res.headers.entries()) {\n if (keepHeaders.includes(k.toLowerCase())) {\n resHeaders.set(k, v)\n }\n }\n logger.debug(\"STATUS: \" + res.status + \" \" + res.statusText)\n\n // Handle non-streaming response\n if (!isStream || [400, 404, 429, 529].includes(res.status)) {\n const json = await res.json()\n await rateLimiter?.track()\n const usage = providerInfo.extractUsage(json)\n if (usage) {\n const usageInfo = providerInfo.normalizeUsage(usage)\n const costInfo = calculateCost(modelInfo, usageInfo)\n await trialLimiter?.track(usageInfo)\n await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)\n await providerBudgetTracker?.track(providerInfo.id, providerInfo.budgetPriority, costInfo.totalCostInCent)\n await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)\n await reload(billingSource, authInfo, costInfo)\n json.cost = calculateOccurredCost(billingSource, costInfo)\n }\n if (res.status === 400) {\n logger.metric({ \"error.response\": JSON.stringify(json) })\n }\n if (json.error?.message) {\n json.error.message = `Error from provider${providerInfo.displayName ? ` (${providerInfo.displayName})` : \"\"}: ${json.error.message}`\n }\n\n const responseConverter = createResponseConverter(providerInfo.format, opts.format)\n const body = JSON.stringify(responseConverter(json))\n logger.metric({ response_length: body.length })\n logger.debug(\"RESPONSE: \" + body)\n return new Response(body, {\n status: resStatus,\n statusText: res.statusText,\n headers: resHeaders,\n })\n }\n\n // Handle streaming response\n const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)\n const usageParser = providerInfo.createUsageParser()\n const binaryDecoder = providerInfo.createBinaryStreamDecoder()\n let reader: ReadableStreamDefaultReader<Uint8Array> | undefined\n const stream = new ReadableStream({\n start(c) {\n reader = res.body?.getReader()\n const decoder = new TextDecoder()\n const encoder = new TextEncoder()\n\n let buffer = \"\"\n let responseLength = 0\n let timestampFirstByte = 0\n\n function pump(): Promise<void> {\n return (\n reader?.read().then(async ({ done, value: rawValue }) => {\n if (done) {\n const timestampLastByte = Date.now()\n logger.metric({\n response_length: responseLength,\n \"timestamp.last_byte\": timestampLastByte,\n })\n await rateLimiter?.track()\n const usage = usageParser.retrieve()\n if (usage) {\n const usageInfo = providerInfo.normalizeUsage(usage)\n const costInfo = calculateCost(modelInfo, usageInfo)\n await trialLimiter?.track(usageInfo)\n await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)\n await modelTpsLimiter?.track(\n providerInfo.id,\n providerInfo.model,\n providerInfo.tpsGoal,\n timestampFirstByte,\n timestampLastByte,\n usageInfo,\n )\n await providerBudgetTracker?.track(\n providerInfo.id,\n providerInfo.budgetPriority,\n costInfo.totalCostInCent,\n )\n await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)\n await reload(billingSource, authInfo, costInfo)\n const cost = calculateOccurredCost(billingSource, costInfo)\n c.enqueue(encoder.encode(buildCostChunk(opts.format, cost)))\n }\n c.close()\n return\n }\n\n if (responseLength === 0) {\n timestampFirstByte = Date.now()\n logger.metric({\n time_to_first_byte: timestampFirstByte - startTimestamp,\n \"timestamp.first_byte\": timestampFirstByte,\n })\n }\n\n const value = binaryDecoder ? binaryDecoder(rawValue) : rawValue\n if (!value) return\n\n responseLength += value.length\n buffer += decoder.decode(value, { stream: true })\n\n const parts = buffer.split(/\\r\\n\\r\\n|\\n\\n|\\r\\r/)\n buffer = parts.pop() ?? \"\"\n\n for (let part of parts) {\n logger.debug(\"PART: \" + part)\n\n part = part.trim()\n usageParser.parse(part)\n\n if (providerInfo.format !== opts.format) {\n part = streamConverter(part)\n c.enqueue(encoder.encode(part + \"\\n\\n\"))\n }\n }\n\n if (providerInfo.format === opts.format) {\n c.enqueue(value)\n }\n\n return pump()\n }) || Promise.resolve()\n )\n }\n\n return pump()\n },\n cancel() {\n // When the downstream caller stops reading, release the upstream\n // response body instead of keeping the provider/inference stream alive.\n return reader?.cancel()\n },\n })\n return new Response(stream, {\n status: resStatus,\n statusText: res.statusText,\n headers: resHeaders,\n })\n } catch (error: any) {\n // The caller disconnected before we finished. Because the outbound provider\n // request shares input.request.signal, an aborted caller surfaces here as an\n // AbortError. There is no client left to receive a body, so skip the error\n // metric and 500 and return a quiet client-closed response.\n if (input.request.signal.aborted || error?.name === \"AbortError\") {\n logger.debug(\"REQUEST ABORTED BY CALLER\")\n return new Response(null, { status: 499 })\n }\n\n logger.metric({\n \"error.type\": error.constructor.name,\n \"error.message\": error.message,\n \"error.cause\": error.cause?.toString(),\n })\n if (error.message.startsWith(\"Failed query\")) {\n try {\n logger.metric({\n \"error.cause2\": JSON.stringify(error.cause),\n })\n } catch {}\n }\n\n if (error instanceof RegionError)\n return new Response(\n JSON.stringify({\n type: \"error\",\n error: { type: error.constructor.name, message: error.message },\n }),\n { status: 403 },\n )\n\n // Note: both top level \"type\" and \"error.type\" fields are used by the @ai-sdk/anthropic client to render the error message.\n if (\n error instanceof AuthError ||\n error instanceof CreditsError ||\n error instanceof MonthlyLimitError ||\n error instanceof UserLimitError ||\n error instanceof ModelError\n )\n return new Response(\n JSON.stringify({\n type: \"error\",\n error: { type: error.constructor.name, message: error.message },\n }),\n { status: 401 },\n )\n\n if (\n error instanceof RateLimitError ||\n error instanceof FreeUsageLimitError ||\n error instanceof GoUsageLimitError ||\n error instanceof BlackUsageLimitError\n ) {\n const headers = new Headers()\n if (error.retryAfter) {\n headers.set(\"retry-after\", String(error.retryAfter))\n }\n return new Response(\n JSON.stringify({\n type: \"error\",\n error: {\n type: error.constructor.name,\n message: error.message,\n },\n metadata:\n error instanceof GoUsageLimitError\n ? {\n workspace: error.workspace,\n limitName: error.limitName,\n }\n : {},\n }),\n { status: 429, headers },\n )\n }\n\n return new Response(\n JSON.stringify({\n type: \"error\",\n error: {\n type: \"error\",\n message: \"Internal server error\",\n },\n }),\n { status: 500 },\n )\n }\n\n function validateModel(zenData: ZenData, reqModel: string) {\n if (!(reqModel in zenData.models)) throw new ModelError(t(\"zen.api.error.modelNotSupported\", { model: reqModel }))\n\n const modelId = reqModel\n const modelData = Array.isArray(zenData.models[modelId])\n ? zenData.models[modelId].find((model) => opts.format === model.formatFilter)\n : zenData.models[modelId]\n\n if (!modelData)\n throw new ModelError(\n t(\"zen.api.error.modelFormatNotSupported\", {\n model: reqModel,\n format: opts.format,\n }),\n )\n\n if (modelData.trialEnded)\n throw new ModelError(\n `${t(\"zen.api.error.trialEnded\", {\n model: modelData.name,\n link: \"https://opencode.ai/go\",\n })}`,\n )\n\n logger.metric({ model: modelId })\n\n return { id: modelId, ...modelData }\n }\n\n function selectProvider(\n reqModel: string,\n zenData: ZenData,\n authInfo: AuthInfo,\n modelInfo: ModelInfo,\n stickyId: string,\n trialProviders: string[] | undefined,\n retry: RetryOptions,\n stickyProviderId: string | undefined,\n modelTpmLimits: Record<string, number> | undefined,\n modelTpsLimits: Record<string, { qualify: number; unqualify: number }> | undefined,\n providerBudget:\n | {\n qualify: (providerId: string, priority: number) => boolean\n prefer: (providerId: string, priority: number) => boolean\n }\n | undefined,\n ) {\n const modelProvider = (() => {\n // Byok is top priority b/c if user set their own API key, we should use it\n // instead of using the sticky provider for the same session\n if (authInfo?.provider?.credentials) {\n return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)\n }\n\n // Prioritize trial providers\n let allProviders = modelInfo.providers.filter((provider) => !provider.disabled)\n if (trialProviders) {\n allProviders = allProviders.map((provider) => ({\n ...provider,\n priority: trialProviders.includes(provider.id) ? 0 : provider.priority,\n }))\n }\n\n // Use fallback provider if max retries reached\n const fallbackProvider = allProviders.find((provider) => provider.id === modelInfo.fallbackProvider)\n if (retry.retryCount === MAX_FAILOVER_RETRIES) return fallbackProvider\n\n let topPriority = Infinity\n const providers = allProviders\n .filter((provider) => provider.weight !== 0)\n .filter((provider) => !retry.excludeProviders.includes(provider.id))\n .filter((provider) => {\n if (provider.budgetPriority === undefined) return true\n if (!providerBudget) return true\n return providerBudget.qualify(provider.id, provider.budgetPriority)\n })\n .filter((provider) => {\n if (!provider.tpmLimit) return true\n const usage = modelTpmLimits?.[`${provider.id}/${provider.model}`] ?? 0\n return usage < provider.tpmLimit * 1_000_000\n })\n .filter((provider) => {\n if (!provider.tpsGoal) return true\n const tps = modelTpsLimits?.[`${provider.id}/${provider.model}/${provider.tpsGoal}`] ?? {\n qualify: 0,\n unqualify: 0,\n }\n const isLowTps = tps.qualify + tps.unqualify > 10 && tps.qualify < tps.unqualify\n return !isLowTps\n })\n .map((provider) => {\n topPriority = Math.min(topPriority, provider.priority)\n return provider\n })\n .filter((p) => p.priority <= topPriority)\n .flatMap((provider) => Array<typeof provider>(provider.weight).fill(provider))\n\n // Use the last 4 characters of session ID to select a provider\n let h = 0\n const l = stickyId.length\n for (let i = l - 4; i < l; i++) {\n h = (h * 31 + stickyId.charCodeAt(i)) | 0 // 32-bit int\n }\n const index = (h >>> 0) % providers.length // make unsigned + range 0..length-1\n const provider = providers[index || 0] ?? fallbackProvider\n\n // sticky provider does not exist => use selected provider\n if (!stickyProviderId) return provider\n const stickProvider = allProviders.find((provider) => provider.id === stickyProviderId)\n if (!stickProvider) return provider\n\n const preferBudgetProvider =\n provider.budgetPriority !== undefined && providerBudget?.prefer(provider.id, provider.budgetPriority)\n\n const preferTpsProvider = (() => {\n if (!provider.tpsGoal) return false\n const tps = modelTpsLimits?.[`${provider.id}/${provider.model}/${provider.tpsGoal}`] ?? {\n qualify: 0,\n unqualify: 0,\n }\n return tps.qualify > tps.unqualify * 3\n })()\n\n if (!preferBudgetProvider && !preferTpsProvider) return stickProvider\n\n return provider\n })()\n\n if (!modelProvider) throw new ModelError(t(\"zen.api.error.noProviderAvailable\"))\n if (!(modelProvider.id in zenData.providers))\n throw new ModelError(t(\"zen.api.error.providerNotSupported\", { provider: modelProvider.id }))\n\n return {\n ...modelProvider,\n ...zenData.providers[modelProvider.id],\n ...(() => {\n const providerProps = zenData.providers[modelProvider.id]\n const format = providerProps.format\n const opts = {\n reqModel,\n providerModel: modelProvider.model,\n adjustCacheUsage: providerProps.adjustCacheUsage,\n workspaceID: authInfo?.workspaceID,\n }\n if (format === \"anthropic\") return anthropicHelper(opts)\n if (format === \"google\") return googleHelper(opts)\n if (format === \"openai\") return openaiHelper(opts)\n return oaCompatHelper(opts)\n })(),\n }\n }\n\n async function authenticate(modelInfo: ModelInfo, zenApiKey?: string) {\n if (!zenApiKey) {\n if (modelInfo.allowAnonymous) return\n throw new AuthError(t(\"zen.api.error.missingApiKey\"))\n }\n\n const data = await Database.use((tx) =>\n tx\n .select({\n apiKey: KeyTable.id,\n workspace: {\n id: WorkspaceTable.id,\n region: WorkspaceTable.region,\n isBlocked: WorkspaceTable.is_blocked,\n isFlaggedByAnthropic: WorkspaceTable.is_flagged_by_anthropic,\n isFlaggedByOpenAI: WorkspaceTable.is_flagged_by_openai,\n },\n billing: {\n balance: BillingTable.balance,\n paymentMethodID: BillingTable.paymentMethodID,\n monthlyLimit: BillingTable.monthlyLimit,\n monthlyUsage: BillingTable.monthlyUsage,\n timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,\n reloadTrigger: BillingTable.reloadTrigger,\n timeReloadLockedTill: BillingTable.timeReloadLockedTill,\n subscription: BillingTable.subscription,\n lite: BillingTable.lite,\n },\n user: {\n id: UserTable.id,\n monthlyLimit: UserTable.monthlyLimit,\n monthlyUsage: UserTable.monthlyUsage,\n timeMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated,\n },\n black: {\n id: SubscriptionTable.id,\n rollingUsage: SubscriptionTable.rollingUsage,\n fixedUsage: SubscriptionTable.fixedUsage,\n timeRollingUpdated: SubscriptionTable.timeRollingUpdated,\n timeFixedUpdated: SubscriptionTable.timeFixedUpdated,\n },\n lite: {\n id: LiteTable.id,\n timeCreated: LiteTable.timeCreated,\n rollingUsage: LiteTable.rollingUsage,\n weeklyUsage: LiteTable.weeklyUsage,\n monthlyUsage: LiteTable.monthlyUsage,\n timeRollingUpdated: LiteTable.timeRollingUpdated,\n timeWeeklyUpdated: LiteTable.timeWeeklyUpdated,\n timeMonthlyUpdated: LiteTable.timeMonthlyUpdated,\n },\n provider: {\n credentials: ProviderTable.credentials,\n },\n timeDisabled: ModelTable.timeCreated,\n })\n .from(KeyTable)\n .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))\n .innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID))\n .innerJoin(UserTable, and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID)))\n .leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, modelInfo.id)))\n .leftJoin(\n ProviderTable,\n modelInfo.byokProvider\n ? and(\n eq(ProviderTable.workspaceID, KeyTable.workspaceID),\n eq(ProviderTable.provider, modelInfo.byokProvider),\n )\n : sql`false`,\n )\n .leftJoin(\n SubscriptionTable,\n and(\n eq(SubscriptionTable.workspaceID, KeyTable.workspaceID),\n eq(SubscriptionTable.userID, KeyTable.userID),\n isNull(SubscriptionTable.timeDeleted),\n ),\n )\n .leftJoin(\n LiteTable,\n and(\n eq(LiteTable.workspaceID, KeyTable.workspaceID),\n eq(LiteTable.userID, KeyTable.userID),\n isNull(LiteTable.timeDeleted),\n ),\n )\n .where(and(eq(KeyTable.key, zenApiKey), isNull(KeyTable.timeDeleted)))\n .then((rows) => rows[0]),\n )\n\n if (!data) throw new AuthError(t(\"zen.api.error.invalidApiKey\"))\n if (\n data.workspace.isBlocked ||\n (data.workspace.isFlaggedByAnthropic && modelInfo.id.startsWith(\"claude-\")) ||\n (data.workspace.isFlaggedByOpenAI && modelInfo.id.startsWith(\"gpt-\"))\n )\n throw new AuthError(t(\"zen.api.error.requestBlockedByUpstreamProvider\"))\n if (\n modelInfo.id.startsWith(\"alpha-\") &&\n Resource.App.stage === \"production\" &&\n !ADMIN_WORKSPACES.includes(data.workspace.id)\n )\n throw new AuthError(t(\"zen.api.error.modelNotSupported\", { model: modelInfo.id }))\n\n logger.metric({\n api_key: data.apiKey,\n workspace: data.workspace.id,\n user_id: data.user.id,\n ...(() => {\n if (data.billing.subscription)\n return {\n subscription: data.billing.subscription.plan,\n }\n if (data.billing.lite)\n return {\n subscription: \"lite\",\n }\n return {}\n })(),\n })\n\n return {\n apiKeyId: data.apiKey,\n workspaceID: data.workspace.id,\n region: data.workspace.region,\n billing: data.billing,\n user: data.user,\n black: data.black,\n lite: data.lite,\n provider: data.provider,\n isFree: ADMIN_WORKSPACES.includes(data.workspace.id),\n isDisabled: !!data.timeDisabled,\n }\n }\n\n function validateBilling(authInfo: AuthInfo, modelInfo: ModelInfo): BillingSource {\n if (!authInfo) return \"anonymous\"\n if (authInfo.provider?.credentials) return \"byok\"\n if (authInfo.isFree) return \"free\"\n if (modelInfo.allowAnonymous) return \"free\"\n\n const formatRetryTime = (seconds: number) => {\n const days = Math.floor(seconds / 86400)\n if (days >= 1) return `${days} day${days > 1 ? \"s\" : \"\"}`\n const hours = Math.floor(seconds / 3600)\n const minutes = Math.ceil((seconds % 3600) / 60)\n if (hours >= 1) return `${hours}hr ${minutes}min`\n return `${minutes}min`\n }\n\n // Validate black subscription billing\n if (authInfo.billing.subscription && authInfo.black) {\n try {\n const sub = authInfo.black\n const plan = authInfo.billing.subscription.plan\n\n // Check weekly limit\n if (sub.fixedUsage && sub.timeFixedUpdated) {\n const blackData = BlackData.getLimits({ plan })\n const result = Subscription.analyzeWeeklyUsage({\n limit: blackData.fixedLimit,\n usage: sub.fixedUsage,\n timeUpdated: sub.timeFixedUpdated,\n })\n if (result.status === \"rate-limited\")\n throw new BlackUsageLimitError(\n t(\"zen.api.error.subscriptionQuotaExceeded\", {\n retryIn: formatRetryTime(result.resetInSec),\n }),\n result.resetInSec,\n )\n }\n\n // Check rolling limit\n if (sub.rollingUsage && sub.timeRollingUpdated) {\n const blackData = BlackData.getLimits({ plan })\n const result = Subscription.analyzeRollingUsage({\n limit: blackData.rollingLimit,\n window: blackData.rollingWindow,\n usage: sub.rollingUsage,\n timeUpdated: sub.timeRollingUpdated,\n })\n if (result.status === \"rate-limited\")\n throw new BlackUsageLimitError(\n t(\"zen.api.error.subscriptionQuotaExceeded\", {\n retryIn: formatRetryTime(result.resetInSec),\n }),\n result.resetInSec,\n )\n }\n\n return \"subscription\"\n } catch (e) {\n if (!authInfo.billing.subscription.useBalance) throw e\n }\n }\n\n // Validate lite subscription billing\n if (opts.modelList === \"lite\" && authInfo.billing.lite && authInfo.lite) {\n try {\n const consoleGoUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/go`\n const sub = authInfo.lite\n const liteData = LiteData.getLimits()\n\n // Check weekly limit\n if (sub.weeklyUsage && sub.timeWeeklyUpdated) {\n const result = Subscription.analyzeWeeklyUsage({\n limit: liteData.weeklyLimit,\n usage: sub.weeklyUsage,\n timeUpdated: sub.timeWeeklyUpdated,\n })\n if (result.status === \"rate-limited\")\n throw new GoUsageLimitError(\n t(\"zen.api.error.goSubscriptionWeeklyLimitExceeded\", {\n retryIn: formatRetryTime(result.resetInSec),\n consoleGoUrl,\n }),\n authInfo.workspaceID,\n \"weekly\",\n result.resetInSec,\n )\n }\n\n // Check monthly limit\n if (sub.monthlyUsage && sub.timeMonthlyUpdated) {\n const result = Subscription.analyzeMonthlyUsage({\n limit: liteData.monthlyLimit,\n usage: sub.monthlyUsage,\n timeUpdated: sub.timeMonthlyUpdated,\n timeSubscribed: sub.timeCreated,\n })\n if (result.status === \"rate-limited\")\n throw new GoUsageLimitError(\n t(\"zen.api.error.goSubscriptionMonthlyLimitExceeded\", {\n retryIn: formatRetryTime(result.resetInSec),\n consoleGoUrl,\n }),\n authInfo.workspaceID,\n \"monthly\",\n result.resetInSec,\n )\n }\n\n // Check rolling limit\n if (sub.rollingUsage && sub.timeRollingUpdated) {\n const result = Subscription.analyzeRollingUsage({\n limit: liteData.rollingLimit,\n window: liteData.rollingWindow,\n usage: sub.rollingUsage,\n timeUpdated: sub.timeRollingUpdated,\n })\n if (result.status === \"rate-limited\")\n throw new GoUsageLimitError(\n t(\"zen.api.error.goSubscriptionRollingLimitExceeded\", {\n retryIn: formatRetryTime(result.resetInSec),\n consoleGoUrl,\n }),\n authInfo.workspaceID,\n \"5 hour\",\n result.resetInSec,\n )\n }\n\n return \"lite\"\n } catch (e) {\n if (!authInfo.billing.lite.useBalance) throw e\n }\n }\n\n // Validate pay as you go billing\n const billing = authInfo.billing\n const billingUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/billing`\n const membersUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/members`\n if (!billing.paymentMethodID && billing.balance <= 0)\n throw new CreditsError(t(\"zen.api.error.noPaymentMethod\", { billingUrl }))\n if (billing.balance <= 0) throw new CreditsError(t(\"zen.api.error.insufficientBalance\", { billingUrl }))\n\n const now = new Date()\n const currentYear = now.getUTCFullYear()\n const currentMonth = now.getUTCMonth()\n if (\n billing.monthlyLimit &&\n billing.monthlyUsage &&\n billing.timeMonthlyUsageUpdated &&\n billing.monthlyUsage >= centsToMicroCents(billing.monthlyLimit * 100) &&\n currentYear === billing.timeMonthlyUsageUpdated.getUTCFullYear() &&\n currentMonth === billing.timeMonthlyUsageUpdated.getUTCMonth()\n )\n throw new MonthlyLimitError(\n t(\"zen.api.error.workspaceMonthlyLimitReached\", {\n amount: billing.monthlyLimit,\n billingUrl,\n }),\n )\n\n if (\n authInfo.user.monthlyLimit &&\n authInfo.user.monthlyUsage &&\n authInfo.user.timeMonthlyUsageUpdated &&\n authInfo.user.monthlyUsage >= centsToMicroCents(authInfo.user.monthlyLimit * 100) &&\n currentYear === authInfo.user.timeMonthlyUsageUpdated.getUTCFullYear() &&\n currentMonth === authInfo.user.timeMonthlyUsageUpdated.getUTCMonth()\n )\n throw new UserLimitError(\n t(\"zen.api.error.userMonthlyLimitReached\", {\n amount: authInfo.user.monthlyLimit,\n membersUrl,\n }),\n )\n\n return \"balance\"\n }\n\n function validateModelSettings(billingSource: BillingSource, authInfo: AuthInfo) {\n if (billingSource === \"lite\") return\n if (billingSource === \"anonymous\") return\n if (authInfo!.isDisabled) throw new ModelError(t(\"zen.api.error.modelDisabled\"))\n }\n\n function updateProviderKey(authInfo: AuthInfo, providerInfo: ProviderInfo) {\n if (!authInfo?.provider?.credentials) return\n providerInfo.apiKey = authInfo.provider.credentials\n }\n\n async function fetchWithRetryableStatus(url: string, options: RequestInit, retry = { count: 0 }) {\n const res = await fetch(url, options)\n if ([429, 529].includes(res.status) && retry.count < MAX_RETRYABLE_STATUS_RETRIES) {\n await new Promise((resolve) => setTimeout(resolve, Math.pow(2, retry.count) * 500))\n return fetchWithRetryableStatus(url, options, { count: retry.count + 1 })\n }\n return res\n }\n\n function calculateCost(modelInfo: ModelInfo, usageInfo: UsageInfo) {\n const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =\n usageInfo\n\n const modelCost =\n modelInfo.cost200K &&\n inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) > 200_000\n ? modelInfo.cost200K\n : modelInfo.cost\n\n const inputCost = modelCost.input * inputTokens * 100\n const outputCost = modelCost.output * outputTokens * 100\n const cacheReadCost = (() => {\n if (!cacheReadTokens) return undefined\n if (!modelCost.cacheRead) return undefined\n return modelCost.cacheRead * cacheReadTokens * 100\n })()\n const cacheWrite5mCost = (() => {\n if (!cacheWrite5mTokens) return undefined\n if (!modelCost.cacheWrite5m) return undefined\n return modelCost.cacheWrite5m * cacheWrite5mTokens * 100\n })()\n const cacheWrite1hCost = (() => {\n if (!cacheWrite1hTokens) return undefined\n if (!modelCost.cacheWrite1h) return undefined\n return modelCost.cacheWrite1h * cacheWrite1hTokens * 100\n })()\n const totalCostInCent =\n inputCost + outputCost + (cacheReadCost ?? 0) + (cacheWrite5mCost ?? 0) + (cacheWrite1hCost ?? 0)\n return {\n totalCostInCent,\n inputCost,\n outputCost,\n cacheReadCost,\n cacheWrite5mCost,\n cacheWrite1hCost,\n }\n }\n\n function calculateOccurredCost(billingSource: BillingSource, costInfo: CostInfo) {\n return billingSource === \"balance\" ? (costInfo.totalCostInCent / 100).toFixed(8) : \"0\"\n }\n\n async function trackUsage(\n sessionId: string,\n billingSource: BillingSource,\n authInfo: AuthInfo,\n modelInfo: ModelInfo,\n providerInfo: ProviderInfo,\n usageInfo: UsageInfo,\n costInfo: CostInfo,\n ) {\n const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =\n usageInfo\n const { totalCostInCent, inputCost, outputCost, cacheReadCost, cacheWrite5mCost, cacheWrite1hCost } = costInfo\n\n logger.metric({\n \"tokens.input\": inputTokens,\n \"tokens.output\": outputTokens,\n \"tokens.reasoning\": reasoningTokens,\n \"tokens.cache_read\": cacheReadTokens,\n \"tokens.cache_write_5m\": cacheWrite5mTokens,\n \"tokens.cache_write_1h\": cacheWrite1hTokens,\n \"cost.input.microcents\": centsToMicroCents(inputCost),\n \"cost.output.microcents\": centsToMicroCents(outputCost),\n \"cost.cache_read.microcents\": cacheReadCost ? centsToMicroCents(cacheReadCost) : undefined,\n \"cost.cache_write.microcents\": cacheWrite5mCost ? centsToMicroCents(cacheWrite5mCost) : undefined,\n \"cost.total.microcents\": centsToMicroCents(totalCostInCent),\n // deprecated - remove after May 20, 2026\n \"cost.input\": Math.round(inputCost),\n \"cost.output\": Math.round(outputCost),\n \"cost.cache_read\": cacheReadCost ? Math.round(cacheReadCost) : undefined,\n \"cost.cache_write_5m\": cacheWrite5mCost ? Math.round(cacheWrite5mCost) : undefined,\n \"cost.cache_write_1h\": cacheWrite1hCost ? Math.round(cacheWrite1hCost) : undefined,\n \"cost.total\": Math.round(totalCostInCent),\n })\n\n if (billingSource === \"anonymous\") return\n authInfo = authInfo!\n\n const cost = centsToMicroCents(totalCostInCent)\n\n // For hot workspaces, batch balance/usage updates through Redis to avoid\n // row-level lock contention on BillingTable/UserTable. Returns the amount\n // to flush this request, or null to skip the DB writes entirely.\n const balanceFlush = await (async () => {\n if (billingSource !== \"subscription\" && billingSource !== \"lite\" && HOT_WORKSPACES.has(authInfo.workspaceID)) {\n const workspaceCost = billingSource === \"free\" || billingSource === \"byok\" ? 0 : cost\n const flush = await accumulateUsage(authInfo.workspaceID, authInfo.user.id, workspaceCost, cost)\n return { batched: true as const, flush }\n }\n return { batched: false as const, flush: null }\n })()\n\n await Database.use((db) =>\n Promise.all([\n db.insert(UsageTable).values({\n workspaceID: authInfo.workspaceID,\n id: Identifier.create(\"usage\"),\n model: modelInfo.id,\n provider: providerInfo.id,\n inputTokens,\n outputTokens,\n reasoningTokens,\n cacheReadTokens,\n cacheWrite5mTokens,\n cacheWrite1hTokens,\n cost,\n keyID: authInfo.apiKeyId,\n sessionID: sessionId.substring(0, 30),\n enrichment: (() => {\n if (billingSource === \"subscription\") return { plan: \"sub\" }\n if (billingSource === \"byok\") return { plan: \"byok\" }\n if (billingSource === \"lite\") return { plan: \"lite\" }\n return undefined\n })(),\n }),\n ...(() => {\n if (billingSource === \"subscription\") {\n const plan = authInfo.billing.subscription!.plan\n const black = BlackData.getLimits({ plan })\n const week = getWeekBounds(new Date())\n const rollingWindowSeconds = black.rollingWindow * 3600\n return [\n db\n .update(SubscriptionTable)\n .set({\n fixedUsage: sql`\n CASE\n WHEN ${SubscriptionTable.timeFixedUpdated} >= ${week.start} THEN ${SubscriptionTable.fixedUsage} + ${cost}\n ELSE ${cost}\n END\n `,\n timeFixedUpdated: sql`now()`,\n rollingUsage: sql`\n CASE\n WHEN UNIX_TIMESTAMP(${SubscriptionTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${SubscriptionTable.rollingUsage} + ${cost}\n ELSE ${cost}\n END\n `,\n timeRollingUpdated: sql`\n CASE\n WHEN UNIX_TIMESTAMP(${SubscriptionTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${SubscriptionTable.timeRollingUpdated}\n ELSE now()\n END\n `,\n })\n .where(\n and(\n eq(SubscriptionTable.workspaceID, authInfo.workspaceID),\n eq(SubscriptionTable.userID, authInfo.user.id),\n ),\n ),\n ]\n }\n if (billingSource === \"lite\") {\n const lite = LiteData.getLimits()\n const week = getWeekBounds(new Date())\n const month = getMonthlyBounds(new Date(), authInfo.lite!.timeCreated)\n const rollingWindowSeconds = lite.rollingWindow * 3600\n const quotaCost = Math.round(cost * modelInfo.costMultiplier)\n return [\n db\n .update(LiteTable)\n .set({\n monthlyUsage: sql`\n CASE\n WHEN ${LiteTable.timeMonthlyUpdated} >= ${month.start} THEN ${LiteTable.monthlyUsage} + ${quotaCost}\n ELSE ${quotaCost}\n END\n `,\n timeMonthlyUpdated: sql`now()`,\n weeklyUsage: sql`\n CASE\n WHEN ${LiteTable.timeWeeklyUpdated} >= ${week.start} THEN ${LiteTable.weeklyUsage} + ${quotaCost}\n ELSE ${quotaCost}\n END\n `,\n timeWeeklyUpdated: sql`now()`,\n rollingUsage: sql`\n CASE\n WHEN UNIX_TIMESTAMP(${LiteTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${LiteTable.rollingUsage} + ${quotaCost}\n ELSE ${quotaCost}\n END\n `,\n timeRollingUpdated: sql`\n CASE\n WHEN UNIX_TIMESTAMP(${LiteTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${LiteTable.timeRollingUpdated}\n ELSE now()\n END\n `,\n })\n .where(and(eq(LiteTable.workspaceID, authInfo.workspaceID), eq(LiteTable.userID, authInfo.user.id))),\n ]\n }\n\n // Batched hot workspace: skip DB writes unless this request is the flush.\n if (balanceFlush.batched && !balanceFlush.flush) return []\n\n const workspaceDelta = balanceFlush.flush?.workspaceCost ?? cost\n const userDelta = balanceFlush.flush?.userCost ?? cost\n const balanceDelta = billingSource === \"free\" || billingSource === \"byok\" ? 0 : workspaceDelta\n\n return [\n db\n .update(BillingTable)\n .set({\n balance: sql`${BillingTable.balance} - ${balanceDelta}`,\n monthlyUsage: sql`\n CASE\n WHEN MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${workspaceDelta}\n ELSE ${workspaceDelta}\n END\n `,\n timeMonthlyUsageUpdated: sql`now()`,\n })\n .where(eq(BillingTable.workspaceID, authInfo.workspaceID)),\n db\n .update(UserTable)\n .set({\n monthlyUsage: sql`\n CASE\n WHEN MONTH(${UserTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${UserTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${UserTable.monthlyUsage} + ${userDelta}\n ELSE ${userDelta}\n END\n `,\n timeMonthlyUsageUpdated: sql`now()`,\n })\n .where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id))),\n ]\n })(),\n ]),\n )\n\n return { costInMicroCents: cost }\n }\n\n async function reload(billingSource: BillingSource, authInfo: AuthInfo, costInfo: CostInfo) {\n if (billingSource !== \"balance\") return\n authInfo = authInfo!\n\n const reloadTrigger = centsToMicroCents((authInfo.billing.reloadTrigger ?? Billing.RELOAD_TRIGGER) * 100)\n if (authInfo.billing.balance - costInfo.totalCostInCent >= reloadTrigger) return\n if (authInfo.billing.timeReloadLockedTill && authInfo.billing.timeReloadLockedTill > new Date()) return\n\n const lock = await Database.use((tx) =>\n tx\n .update(BillingTable)\n .set({\n timeReloadLockedTill: sql`now() + interval 1 minute`,\n })\n .where(\n and(\n eq(BillingTable.workspaceID, authInfo.workspaceID),\n eq(BillingTable.reload, true),\n lt(BillingTable.balance, reloadTrigger),\n or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)),\n ),\n ),\n )\n if (lock.rowsAffected === 0) return\n\n await Actor.provide(\"system\", { workspaceID: authInfo.workspaceID }, async () => {\n await Billing.reload()\n })\n }\n}\n"}}}} |