mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-30 05:43:55 +00:00
wip: zen
This commit is contained in:
parent
9f150b0776
commit
6aa4928e9e
@ -118,7 +118,6 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
|
||||
price: zenLitePrice.id,
|
||||
},
|
||||
})
|
||||
const ZEN_LITE_LIMITS = new sst.Secret("ZEN_LITE_LIMITS")
|
||||
|
||||
const zenBlackProduct = new stripe.Product("ZenBlack", {
|
||||
name: "OpenCode Black",
|
||||
@ -142,7 +141,6 @@ const ZEN_BLACK_PRICE = new sst.Linkable("ZEN_BLACK_PRICE", {
|
||||
plan20: zenBlackPrice20.id,
|
||||
},
|
||||
})
|
||||
const ZEN_BLACK_LIMITS = new sst.Secret("ZEN_BLACK_LIMITS")
|
||||
|
||||
const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS1"),
|
||||
@ -215,9 +213,8 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
AWS_SES_ACCESS_KEY_ID,
|
||||
AWS_SES_SECRET_ACCESS_KEY,
|
||||
ZEN_BLACK_PRICE,
|
||||
ZEN_BLACK_LIMITS,
|
||||
ZEN_LITE_PRICE,
|
||||
ZEN_LITE_LIMITS,
|
||||
new sst.Secret("ZEN_LIMITS"),
|
||||
new sst.Secret("ZEN_SESSION_SECRET"),
|
||||
...ZEN_MODELS,
|
||||
...($dev
|
||||
|
||||
@ -97,9 +97,9 @@ export async function handler(
|
||||
const zenData = ZenData.list(opts.modelList)
|
||||
const modelInfo = validateModel(zenData, model)
|
||||
const dataDumper = createDataDumper(sessionId, requestId, projectId)
|
||||
const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient)
|
||||
const isTrial = await trialLimiter?.isTrial()
|
||||
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip, input.request)
|
||||
const trialLimiter = createTrialLimiter(modelInfo.trialProvider, ip)
|
||||
const trialProvider = await trialLimiter?.check()
|
||||
const rateLimiter = createRateLimiter(modelInfo.allowAnonymous, ip, input.request)
|
||||
await rateLimiter?.check()
|
||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
||||
const stickyProvider = await stickyTracker?.get()
|
||||
@ -114,7 +114,7 @@ export async function handler(
|
||||
authInfo,
|
||||
modelInfo,
|
||||
sessionId,
|
||||
isTrial ?? false,
|
||||
trialProvider,
|
||||
retry,
|
||||
stickyProvider,
|
||||
)
|
||||
@ -144,9 +144,6 @@ export async function handler(
|
||||
Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => {
|
||||
headers.set(k, headers.get(v)!)
|
||||
})
|
||||
Object.entries(providerInfo.headers ?? {}).forEach(([k, v]) => {
|
||||
headers.set(k, v)
|
||||
})
|
||||
headers.delete("host")
|
||||
headers.delete("content-length")
|
||||
headers.delete("x-opencode-request")
|
||||
@ -295,18 +292,13 @@ export async function handler(
|
||||
part = part.trim()
|
||||
usageParser.parse(part)
|
||||
|
||||
if (providerInfo.responseModifier) {
|
||||
for (const [k, v] of Object.entries(providerInfo.responseModifier)) {
|
||||
part = part.replace(k, v)
|
||||
}
|
||||
c.enqueue(encoder.encode(part + "\n\n"))
|
||||
} else if (providerInfo.format !== opts.format) {
|
||||
if (providerInfo.format !== opts.format) {
|
||||
part = streamConverter(part)
|
||||
c.enqueue(encoder.encode(part + "\n\n"))
|
||||
}
|
||||
}
|
||||
|
||||
if (!providerInfo.responseModifier && providerInfo.format === opts.format) {
|
||||
if (providerInfo.format === opts.format) {
|
||||
c.enqueue(value)
|
||||
}
|
||||
|
||||
@ -398,7 +390,7 @@ export async function handler(
|
||||
authInfo: AuthInfo,
|
||||
modelInfo: ModelInfo,
|
||||
sessionId: string,
|
||||
isTrial: boolean,
|
||||
trialProvider: string | undefined,
|
||||
retry: RetryOptions,
|
||||
stickyProvider: string | undefined,
|
||||
) {
|
||||
@ -407,8 +399,8 @@ export async function handler(
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
|
||||
}
|
||||
|
||||
if (isTrial) {
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.trial!.provider)
|
||||
if (trialProvider) {
|
||||
return modelInfo.providers.find((provider) => provider.id === trialProvider)
|
||||
}
|
||||
|
||||
if (stickyProvider) {
|
||||
|
||||
@ -2,29 +2,28 @@ import { Database, eq, and, sql, inArray } from "@opencode-ai/console-core/drizz
|
||||
import { IpRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
import { FreeUsageLimitError } from "./error"
|
||||
import { logger } from "./logger"
|
||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
import { Subscription } from "@opencode-ai/console-core/subscription.js"
|
||||
|
||||
export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, request: Request) {
|
||||
if (!limit) return
|
||||
export function createRateLimiter(allowAnonymous: boolean | undefined, rawIp: string, request: Request) {
|
||||
if (!allowAnonymous) return
|
||||
const dict = i18n(localeFromRequest(request))
|
||||
|
||||
const limitValue = limit.checkHeader && !request.headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value
|
||||
const limits = Subscription.getFreeLimits()
|
||||
const limitValue =
|
||||
limits.checkHeader && !request.headers.get(limits.checkHeader) ? limits.fallbackValue : limits.dailyRequests
|
||||
|
||||
const ip = !rawIp.length ? "unknown" : rawIp
|
||||
const now = Date.now()
|
||||
const intervals =
|
||||
limit.period === "day"
|
||||
? [buildYYYYMMDD(now)]
|
||||
: [buildYYYYMMDDHH(now), buildYYYYMMDDHH(now - 3_600_000), buildYYYYMMDDHH(now - 7_200_000)]
|
||||
const interval = buildYYYYMMDD(now)
|
||||
|
||||
return {
|
||||
track: async () => {
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.insert(IpRateLimitTable)
|
||||
.values({ ip, interval: intervals[0], count: 1 })
|
||||
.values({ ip, interval, count: 1 })
|
||||
.onDuplicateKeyUpdate({ set: { count: sql`${IpRateLimitTable.count} + 1` } }),
|
||||
)
|
||||
},
|
||||
@ -33,15 +32,12 @@ export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: s
|
||||
tx
|
||||
.select({ interval: IpRateLimitTable.interval, count: IpRateLimitTable.count })
|
||||
.from(IpRateLimitTable)
|
||||
.where(and(eq(IpRateLimitTable.ip, ip), inArray(IpRateLimitTable.interval, intervals))),
|
||||
.where(and(eq(IpRateLimitTable.ip, ip), inArray(IpRateLimitTable.interval, [interval]))),
|
||||
)
|
||||
const total = rows.reduce((sum, r) => sum + r.count, 0)
|
||||
logger.debug(`rate limit total: ${total}`)
|
||||
if (total >= limitValue)
|
||||
throw new FreeUsageLimitError(
|
||||
dict["zen.api.error.rateLimitExceeded"],
|
||||
limit.period === "day" ? getRetryAfterDay(now) : getRetryAfterHour(rows, intervals, limitValue, now),
|
||||
)
|
||||
throw new FreeUsageLimitError(dict["zen.api.error.rateLimitExceeded"], getRetryAfterDay(now))
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -50,37 +46,9 @@ export function getRetryAfterDay(now: number) {
|
||||
return Math.ceil((86_400_000 - (now % 86_400_000)) / 1000)
|
||||
}
|
||||
|
||||
export function getRetryAfterHour(
|
||||
rows: { interval: string; count: number }[],
|
||||
intervals: string[],
|
||||
limit: number,
|
||||
now: number,
|
||||
) {
|
||||
const counts = new Map(rows.map((r) => [r.interval, r.count]))
|
||||
// intervals are ordered newest to oldest: [current, -1h, -2h]
|
||||
// simulate dropping oldest intervals one at a time
|
||||
let running = intervals.reduce((sum, i) => sum + (counts.get(i) ?? 0), 0)
|
||||
for (let i = intervals.length - 1; i >= 0; i--) {
|
||||
running -= counts.get(intervals[i]) ?? 0
|
||||
if (running < limit) {
|
||||
// interval at index i rolls out of the window (intervals.length - i) hours from the current hour start
|
||||
const hours = intervals.length - i
|
||||
return Math.ceil((hours * 3_600_000 - (now % 3_600_000)) / 1000)
|
||||
}
|
||||
}
|
||||
return Math.ceil((3_600_000 - (now % 3_600_000)) / 1000)
|
||||
}
|
||||
|
||||
function buildYYYYMMDD(timestamp: number) {
|
||||
return new Date(timestamp)
|
||||
.toISOString()
|
||||
.replace(/[^0-9]/g, "")
|
||||
.substring(0, 8)
|
||||
}
|
||||
|
||||
function buildYYYYMMDDHH(timestamp: number) {
|
||||
return new Date(timestamp)
|
||||
.toISOString()
|
||||
.replace(/[^0-9]/g, "")
|
||||
.substring(0, 10)
|
||||
}
|
||||
|
||||
@ -1,21 +1,18 @@
|
||||
import { Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { IpTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
import { UsageInfo } from "./provider/provider"
|
||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||
import { Subscription } from "@opencode-ai/console-core/subscription.js"
|
||||
|
||||
export function createTrialLimiter(trial: ZenData.Trial | undefined, ip: string, client: string) {
|
||||
if (!trial) return
|
||||
export function createTrialLimiter(trialProvider: string | undefined, ip: string) {
|
||||
if (!trialProvider) return
|
||||
if (!ip) return
|
||||
|
||||
const limit =
|
||||
trial.limits.find((limit) => limit.client === client)?.limit ??
|
||||
trial.limits.find((limit) => limit.client === undefined)?.limit
|
||||
if (!limit) return
|
||||
const limit = Subscription.getFreeLimits().promoTokens
|
||||
|
||||
let _isTrial: boolean
|
||||
|
||||
return {
|
||||
isTrial: async () => {
|
||||
check: async () => {
|
||||
const data = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
@ -27,7 +24,7 @@ export function createTrialLimiter(trial: ZenData.Trial | undefined, ip: string,
|
||||
)
|
||||
|
||||
_isTrial = (data?.usage ?? 0) < limit
|
||||
return _isTrial
|
||||
return _isTrial ? trialProvider : undefined
|
||||
},
|
||||
track: async (usageInfo: UsageInfo) => {
|
||||
if (!_isTrial) return
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { getRetryAfterDay, getRetryAfterHour } from "../src/routes/zen/util/rateLimiter"
|
||||
import { getRetryAfterDay } from "../src/routes/zen/util/rateLimiter"
|
||||
|
||||
describe("getRetryAfterDay", () => {
|
||||
test("returns full day at midnight UTC", () => {
|
||||
@ -17,76 +17,3 @@ describe("getRetryAfterDay", () => {
|
||||
expect(getRetryAfterDay(almost)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getRetryAfterHour", () => {
|
||||
// 14:30:00 UTC — 30 minutes into the current hour
|
||||
const now = Date.UTC(2026, 0, 15, 14, 30, 0, 0)
|
||||
const intervals = ["2026011514", "2026011513", "2026011512"]
|
||||
|
||||
test("waits 3 hours when all usage is in current hour", () => {
|
||||
const rows = [{ interval: "2026011514", count: 10 }]
|
||||
// only current hour has usage — it won't leave the window for 3 hours from hour start
|
||||
// 3 * 3600 - 1800 = 9000s
|
||||
expect(getRetryAfterHour(rows, intervals, 10, now)).toBe(9000)
|
||||
})
|
||||
|
||||
test("waits 1 hour when dropping oldest interval is sufficient", () => {
|
||||
const rows = [
|
||||
{ interval: "2026011514", count: 2 },
|
||||
{ interval: "2026011512", count: 10 },
|
||||
]
|
||||
// total=12, drop oldest (-2h, count=10) -> 2 < 10
|
||||
// hours = 3 - 2 = 1 -> 1 * 3600 - 1800 = 1800s
|
||||
expect(getRetryAfterHour(rows, intervals, 10, now)).toBe(1800)
|
||||
})
|
||||
|
||||
test("waits 2 hours when usage spans oldest two intervals", () => {
|
||||
const rows = [
|
||||
{ interval: "2026011513", count: 8 },
|
||||
{ interval: "2026011512", count: 5 },
|
||||
]
|
||||
// total=13, drop -2h (5) -> 8, 8 >= 8, drop -1h (8) -> 0 < 8
|
||||
// hours = 3 - 1 = 2 -> 2 * 3600 - 1800 = 5400s
|
||||
expect(getRetryAfterHour(rows, intervals, 8, now)).toBe(5400)
|
||||
})
|
||||
|
||||
test("waits 1 hour when oldest interval alone pushes over limit", () => {
|
||||
const rows = [
|
||||
{ interval: "2026011514", count: 1 },
|
||||
{ interval: "2026011513", count: 1 },
|
||||
{ interval: "2026011512", count: 10 },
|
||||
]
|
||||
// total=12, drop -2h (10) -> 2 < 10
|
||||
// hours = 3 - 2 = 1 -> 1800s
|
||||
expect(getRetryAfterHour(rows, intervals, 10, now)).toBe(1800)
|
||||
})
|
||||
|
||||
test("waits 2 hours when middle interval keeps total over limit", () => {
|
||||
const rows = [
|
||||
{ interval: "2026011514", count: 4 },
|
||||
{ interval: "2026011513", count: 4 },
|
||||
{ interval: "2026011512", count: 4 },
|
||||
]
|
||||
// total=12, drop -2h (4) -> 8, 8 >= 5, drop -1h (4) -> 4 < 5
|
||||
// hours = 3 - 1 = 2 -> 5400s
|
||||
expect(getRetryAfterHour(rows, intervals, 5, now)).toBe(5400)
|
||||
})
|
||||
|
||||
test("rounds up to nearest second", () => {
|
||||
const offset = Date.UTC(2026, 0, 15, 14, 30, 0, 500)
|
||||
const rows = [
|
||||
{ interval: "2026011514", count: 2 },
|
||||
{ interval: "2026011512", count: 10 },
|
||||
]
|
||||
// hours=1 -> 3_600_000 - 1_800_500 = 1_799_500ms -> ceil(1799.5) = 1800
|
||||
expect(getRetryAfterHour(rows, intervals, 10, offset)).toBe(1800)
|
||||
})
|
||||
|
||||
test("fallback returns time until next hour when rows are empty", () => {
|
||||
// edge case: rows empty but function called (shouldn't happen in practice)
|
||||
// loop drops all zeros, running stays 0 which is < any positive limit on first iteration
|
||||
const rows: { interval: string; count: number }[] = []
|
||||
// drop -2h (0) -> 0 < 1 -> hours = 3 - 2 = 1 -> 1800s
|
||||
expect(getRetryAfterHour(rows, intervals, 1, now)).toBe(1800)
|
||||
})
|
||||
})
|
||||
|
||||
@ -34,12 +34,9 @@
|
||||
"promote-models-to-prod": "script/promote-models.ts production",
|
||||
"pull-models-from-dev": "script/pull-models.ts dev",
|
||||
"pull-models-from-prod": "script/pull-models.ts production",
|
||||
"update-black": "script/update-black.ts",
|
||||
"promote-black-to-dev": "script/promote-black.ts dev",
|
||||
"promote-black-to-prod": "script/promote-black.ts production",
|
||||
"update-lite": "script/update-lite.ts",
|
||||
"promote-lite-to-dev": "script/promote-lite.ts dev",
|
||||
"promote-lite-to-prod": "script/promote-lite.ts production",
|
||||
"update-limits": "script/update-limits.ts",
|
||||
"promote-limits-to-dev": "script/promote-limits.ts dev",
|
||||
"promote-limits-to-prod": "script/promote-limits.ts production",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
312
packages/console/core/script/black-stats.ts
Normal file
312
packages/console/core/script/black-stats.ts
Normal file
@ -0,0 +1,312 @@
|
||||
import { Database, and, eq, inArray, isNotNull, sql } from "../src/drizzle/index.js"
|
||||
import { BillingTable, BlackPlans, SubscriptionTable, UsageTable } from "../src/schema/billing.sql.js"
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
console.error("Usage: bun black-stats.ts <plan>")
|
||||
process.exit(1)
|
||||
}
|
||||
const plan = process.argv[2] as (typeof BlackPlans)[number]
|
||||
if (!BlackPlans.includes(plan)) {
|
||||
console.error("Usage: bun black-stats.ts <plan>")
|
||||
process.exit(1)
|
||||
}
|
||||
const cutoff = new Date(Date.UTC(2026, 1, 0, 23, 59, 59, 999))
|
||||
|
||||
// get workspaces
|
||||
const workspaces = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ workspaceID: BillingTable.workspaceID })
|
||||
.from(BillingTable)
|
||||
.where(
|
||||
and(isNotNull(BillingTable.subscriptionID), sql`JSON_UNQUOTE(JSON_EXTRACT(subscription, '$.plan')) = ${plan}`),
|
||||
),
|
||||
)
|
||||
if (workspaces.length === 0) throw new Error(`No active Black ${plan} subscriptions found`)
|
||||
|
||||
const week = sql<number>`YEARWEEK(${UsageTable.timeCreated}, 3)`
|
||||
const workspaceIDs = workspaces.map((row) => row.workspaceID)
|
||||
// Get subscription spend
|
||||
const spend = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
workspaceID: UsageTable.workspaceID,
|
||||
week,
|
||||
amount: sql<number>`COALESCE(SUM(${UsageTable.cost}), 0)`,
|
||||
})
|
||||
.from(UsageTable)
|
||||
.where(
|
||||
and(inArray(UsageTable.workspaceID, workspaceIDs), sql`JSON_UNQUOTE(JSON_EXTRACT(enrichment, '$.plan')) = 'sub'`),
|
||||
)
|
||||
.groupBy(UsageTable.workspaceID, week),
|
||||
)
|
||||
|
||||
// Get pay per use spend
|
||||
const ppu = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
workspaceID: UsageTable.workspaceID,
|
||||
week,
|
||||
amount: sql<number>`COALESCE(SUM(${UsageTable.cost}), 0)`,
|
||||
})
|
||||
.from(UsageTable)
|
||||
.where(
|
||||
and(
|
||||
inArray(UsageTable.workspaceID, workspaceIDs),
|
||||
sql`(${UsageTable.enrichment} IS NULL OR JSON_UNQUOTE(JSON_EXTRACT(enrichment, '$.plan')) != 'sub')`,
|
||||
),
|
||||
)
|
||||
.groupBy(UsageTable.workspaceID, week),
|
||||
)
|
||||
|
||||
const models = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
workspaceID: UsageTable.workspaceID,
|
||||
model: UsageTable.model,
|
||||
amount: sql<number>`COALESCE(SUM(${UsageTable.cost}), 0)`,
|
||||
})
|
||||
.from(UsageTable)
|
||||
.where(
|
||||
and(inArray(UsageTable.workspaceID, workspaceIDs), sql`JSON_UNQUOTE(JSON_EXTRACT(enrichment, '$.plan')) = 'sub'`),
|
||||
)
|
||||
.groupBy(UsageTable.workspaceID, UsageTable.model),
|
||||
)
|
||||
|
||||
const tokens = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
workspaceID: UsageTable.workspaceID,
|
||||
week,
|
||||
input: sql<number>`COALESCE(SUM(${UsageTable.inputTokens}), 0)`,
|
||||
cacheRead: sql<number>`COALESCE(SUM(${UsageTable.cacheReadTokens}), 0)`,
|
||||
output: sql<number>`COALESCE(SUM(${UsageTable.outputTokens}), 0) + COALESCE(SUM(${UsageTable.reasoningTokens}), 0)`,
|
||||
})
|
||||
.from(UsageTable)
|
||||
.where(
|
||||
and(inArray(UsageTable.workspaceID, workspaceIDs), sql`JSON_UNQUOTE(JSON_EXTRACT(enrichment, '$.plan')) = 'sub'`),
|
||||
)
|
||||
.groupBy(UsageTable.workspaceID, week),
|
||||
)
|
||||
|
||||
const allWeeks = [...spend, ...ppu].map((row) => row.week)
|
||||
const weeks = [...new Set(allWeeks)].sort((a, b) => a - b)
|
||||
const spendMap = new Map<string, Map<number, number>>()
|
||||
const totals = new Map<string, number>()
|
||||
const ppuMap = new Map<string, Map<number, number>>()
|
||||
const ppuTotals = new Map<string, number>()
|
||||
const modelMap = new Map<string, { model: string; amount: number }[]>()
|
||||
const tokenMap = new Map<string, Map<number, { input: number; cacheRead: number; output: number }>>()
|
||||
|
||||
for (const row of spend) {
|
||||
const workspace = spendMap.get(row.workspaceID) ?? new Map<number, number>()
|
||||
const total = totals.get(row.workspaceID) ?? 0
|
||||
const amount = toNumber(row.amount)
|
||||
workspace.set(row.week, amount)
|
||||
totals.set(row.workspaceID, total + amount)
|
||||
spendMap.set(row.workspaceID, workspace)
|
||||
}
|
||||
|
||||
for (const row of ppu) {
|
||||
const workspace = ppuMap.get(row.workspaceID) ?? new Map<number, number>()
|
||||
const total = ppuTotals.get(row.workspaceID) ?? 0
|
||||
const amount = toNumber(row.amount)
|
||||
workspace.set(row.week, amount)
|
||||
ppuTotals.set(row.workspaceID, total + amount)
|
||||
ppuMap.set(row.workspaceID, workspace)
|
||||
}
|
||||
|
||||
for (const row of models) {
|
||||
const current = modelMap.get(row.workspaceID) ?? []
|
||||
current.push({ model: row.model, amount: toNumber(row.amount) })
|
||||
modelMap.set(row.workspaceID, current)
|
||||
}
|
||||
|
||||
for (const row of tokens) {
|
||||
const workspace = tokenMap.get(row.workspaceID) ?? new Map()
|
||||
workspace.set(row.week, {
|
||||
input: toNumber(row.input),
|
||||
cacheRead: toNumber(row.cacheRead),
|
||||
output: toNumber(row.output),
|
||||
})
|
||||
tokenMap.set(row.workspaceID, workspace)
|
||||
}
|
||||
|
||||
const users = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
workspaceID: SubscriptionTable.workspaceID,
|
||||
subscribed: SubscriptionTable.timeCreated,
|
||||
subscription: BillingTable.subscription,
|
||||
})
|
||||
.from(SubscriptionTable)
|
||||
.innerJoin(BillingTable, eq(SubscriptionTable.workspaceID, BillingTable.workspaceID))
|
||||
.where(
|
||||
and(inArray(SubscriptionTable.workspaceID, workspaceIDs), sql`${SubscriptionTable.timeCreated} <= ${cutoff}`),
|
||||
),
|
||||
)
|
||||
|
||||
const counts = new Map<string, number>()
|
||||
for (const user of users) {
|
||||
const current = counts.get(user.workspaceID) ?? 0
|
||||
counts.set(user.workspaceID, current + 1)
|
||||
}
|
||||
|
||||
const rows = users
|
||||
.map((user) => {
|
||||
const workspace = spendMap.get(user.workspaceID) ?? new Map<number, number>()
|
||||
const ppuWorkspace = ppuMap.get(user.workspaceID) ?? new Map<number, number>()
|
||||
const count = counts.get(user.workspaceID) ?? 1
|
||||
const amount = (totals.get(user.workspaceID) ?? 0) / count
|
||||
const ppuAmount = (ppuTotals.get(user.workspaceID) ?? 0) / count
|
||||
const monthStart = user.subscribed ? startOfMonth(user.subscribed) : null
|
||||
const modelRows = (modelMap.get(user.workspaceID) ?? []).sort((a, b) => b.amount - a.amount).slice(0, 3)
|
||||
const modelTotal = totals.get(user.workspaceID) ?? 0
|
||||
const modelCells = modelRows.map((row) => ({
|
||||
model: row.model,
|
||||
percent: modelTotal > 0 ? `${((row.amount / modelTotal) * 100).toFixed(1)}%` : "0.0%",
|
||||
}))
|
||||
const modelData = [0, 1, 2].map((index) => modelCells[index] ?? { model: "-", percent: "-" })
|
||||
const weekly = Object.fromEntries(
|
||||
weeks.map((item) => {
|
||||
const value = (workspace.get(item) ?? 0) / count
|
||||
const beforeMonth = monthStart ? isoWeekStart(item) < monthStart : false
|
||||
return [formatWeek(item), beforeMonth ? "-" : formatMicroCents(value)]
|
||||
}),
|
||||
)
|
||||
const ppuWeekly = Object.fromEntries(
|
||||
weeks.map((item) => {
|
||||
const value = (ppuWorkspace.get(item) ?? 0) / count
|
||||
const beforeMonth = monthStart ? isoWeekStart(item) < monthStart : false
|
||||
return [formatWeek(item), beforeMonth ? "-" : formatMicroCents(value)]
|
||||
}),
|
||||
)
|
||||
const tokenWorkspace = tokenMap.get(user.workspaceID) ?? new Map()
|
||||
const weeklyTokens = Object.fromEntries(
|
||||
weeks.map((item) => {
|
||||
const t = tokenWorkspace.get(item) ?? { input: 0, cacheRead: 0, output: 0 }
|
||||
const beforeMonth = monthStart ? isoWeekStart(item) < monthStart : false
|
||||
return [
|
||||
formatWeek(item),
|
||||
beforeMonth
|
||||
? { input: "-", cacheRead: "-", output: "-" }
|
||||
: {
|
||||
input: Math.round(t.input / count),
|
||||
cacheRead: Math.round(t.cacheRead / count),
|
||||
output: Math.round(t.output / count),
|
||||
},
|
||||
]
|
||||
}),
|
||||
)
|
||||
return {
|
||||
workspaceID: user.workspaceID,
|
||||
useBalance: user.subscription?.useBalance ?? false,
|
||||
subscribed: formatDate(user.subscribed),
|
||||
subscribedAt: user.subscribed?.getTime() ?? 0,
|
||||
amount,
|
||||
ppuAmount,
|
||||
models: modelData,
|
||||
weekly,
|
||||
ppuWeekly,
|
||||
weeklyTokens,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.subscribedAt - b.subscribedAt)
|
||||
|
||||
console.log(`Black ${plan} subscribers: ${rows.length}`)
|
||||
const header = [
|
||||
"workspaceID",
|
||||
"subscribed",
|
||||
"useCredit",
|
||||
"subTotal",
|
||||
"ppuTotal",
|
||||
"model1",
|
||||
"model1%",
|
||||
"model2",
|
||||
"model2%",
|
||||
"model3",
|
||||
"model3%",
|
||||
...weeks.flatMap((item) => [
|
||||
formatWeek(item) + " sub",
|
||||
formatWeek(item) + " ppu",
|
||||
formatWeek(item) + " input",
|
||||
formatWeek(item) + " cache",
|
||||
formatWeek(item) + " output",
|
||||
]),
|
||||
]
|
||||
const lines = [header.map(csvCell).join(",")]
|
||||
for (const row of rows) {
|
||||
const model1 = row.models[0]
|
||||
const model2 = row.models[1]
|
||||
const model3 = row.models[2]
|
||||
const cells = [
|
||||
row.workspaceID,
|
||||
row.subscribed ?? "",
|
||||
row.useBalance ? "yes" : "no",
|
||||
formatMicroCents(row.amount),
|
||||
formatMicroCents(row.ppuAmount),
|
||||
model1.model,
|
||||
model1.percent,
|
||||
model2.model,
|
||||
model2.percent,
|
||||
model3.model,
|
||||
model3.percent,
|
||||
...weeks.flatMap((item) => {
|
||||
const t = row.weeklyTokens[formatWeek(item)] ?? { input: "-", cacheRead: "-", output: "-" }
|
||||
return [
|
||||
row.weekly[formatWeek(item)] ?? "",
|
||||
row.ppuWeekly[formatWeek(item)] ?? "",
|
||||
String(t.input),
|
||||
String(t.cacheRead),
|
||||
String(t.output),
|
||||
]
|
||||
}),
|
||||
]
|
||||
lines.push(cells.map(csvCell).join(","))
|
||||
}
|
||||
const output = `${lines.join("\n")}\n`
|
||||
const file = Bun.file(`black-stats-${plan}.csv`)
|
||||
await file.write(output)
|
||||
console.log(`Wrote ${lines.length - 1} rows to ${file.name}`)
|
||||
const total = rows.reduce((sum, row) => sum + row.amount, 0)
|
||||
const average = rows.length === 0 ? 0 : total / rows.length
|
||||
console.log(`Average spending per user: ${formatMicroCents(average)}`)
|
||||
|
||||
function formatMicroCents(value: number) {
|
||||
return `$${(value / 100000000).toFixed(2)}`
|
||||
}
|
||||
|
||||
function formatDate(value: Date | null | undefined) {
|
||||
if (!value) return null
|
||||
return value.toISOString().split("T")[0]
|
||||
}
|
||||
|
||||
function formatWeek(value: number) {
|
||||
return formatDate(isoWeekStart(value)) ?? ""
|
||||
}
|
||||
|
||||
function startOfMonth(value: Date) {
|
||||
return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), 1))
|
||||
}
|
||||
|
||||
function isoWeekStart(value: number) {
|
||||
const year = Math.floor(value / 100)
|
||||
const weekNumber = value % 100
|
||||
const jan4 = new Date(Date.UTC(year, 0, 4))
|
||||
const day = jan4.getUTCDay() || 7
|
||||
const weekStart = new Date(Date.UTC(year, 0, 4 - (day - 1)))
|
||||
weekStart.setUTCDate(weekStart.getUTCDate() + (weekNumber - 1) * 7)
|
||||
return weekStart
|
||||
}
|
||||
|
||||
function toNumber(value: unknown) {
|
||||
if (typeof value === "number") return value
|
||||
if (typeof value === "bigint") return Number(value)
|
||||
if (typeof value === "string") return Number(value)
|
||||
return 0
|
||||
}
|
||||
|
||||
function csvCell(value: string | number) {
|
||||
const text = String(value)
|
||||
if (!/[",\n]/.test(text)) return text
|
||||
return `"${text.replace(/"/g, '""')}"`
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import path from "path"
|
||||
import { BlackData } from "../src/black"
|
||||
|
||||
const stage = process.argv[2]
|
||||
if (!stage) throw new Error("Stage is required")
|
||||
|
||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
|
||||
// read the secret
|
||||
const ret = await $`bun sst secret list`.cwd(root).text()
|
||||
const lines = ret.split("\n")
|
||||
const value = lines.find((line) => line.startsWith("ZEN_BLACK_LIMITS"))?.split("=")[1]
|
||||
if (!value) throw new Error("ZEN_BLACK_LIMITS not found")
|
||||
|
||||
// validate value
|
||||
BlackData.validate(JSON.parse(value))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_BLACK_LIMITS ${value} --stage ${stage}`
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { $ } from "bun"
|
||||
import path from "path"
|
||||
import { LiteData } from "../src/lite"
|
||||
import { Subscription } from "../src/subscription"
|
||||
|
||||
const stage = process.argv[2]
|
||||
if (!stage) throw new Error("Stage is required")
|
||||
@ -12,11 +12,11 @@ const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
// read the secret
|
||||
const ret = await $`bun sst secret list`.cwd(root).text()
|
||||
const lines = ret.split("\n")
|
||||
const value = lines.find((line) => line.startsWith("ZEN_LITE_LIMITS"))?.split("=")[1]
|
||||
if (!value) throw new Error("ZEN_LITE_LIMITS not found")
|
||||
const value = lines.find((line) => line.startsWith("ZEN_LIMITS"))?.split("=")[1]
|
||||
if (!value) throw new Error("ZEN_LIMITS not found")
|
||||
|
||||
// validate value
|
||||
LiteData.validate(JSON.parse(value))
|
||||
Subscription.validate(JSON.parse(value))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_LITE_LIMITS ${value} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_LIMITS ${value} --stage ${stage}`
|
||||
@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { BlackData } from "../src/black"
|
||||
|
||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
const secrets = await $`bun sst secret list`.cwd(root).text()
|
||||
|
||||
// read value
|
||||
const lines = secrets.split("\n")
|
||||
const oldValue = lines.find((line) => line.startsWith("ZEN_BLACK_LIMITS"))?.split("=")[1] ?? "{}"
|
||||
if (!oldValue) throw new Error("ZEN_BLACK_LIMITS not found")
|
||||
|
||||
// store the prettified json to a temp file
|
||||
const filename = `black-${Date.now()}.json`
|
||||
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
||||
await tempFile.write(JSON.stringify(JSON.parse(oldValue), null, 2))
|
||||
console.log("tempFile", tempFile.name)
|
||||
|
||||
// open temp file in vim and read the file on close
|
||||
await $`vim ${tempFile.name}`
|
||||
const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
|
||||
BlackData.validate(JSON.parse(newValue))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_BLACK_LIMITS ${newValue}`
|
||||
@ -3,18 +3,18 @@
|
||||
import { $ } from "bun"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { LiteData } from "../src/lite"
|
||||
import { Subscription } from "../src/subscription"
|
||||
|
||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
const secrets = await $`bun sst secret list`.cwd(root).text()
|
||||
|
||||
// read value
|
||||
const lines = secrets.split("\n")
|
||||
const oldValue = lines.find((line) => line.startsWith("ZEN_LITE_LIMITS"))?.split("=")[1] ?? "{}"
|
||||
if (!oldValue) throw new Error("ZEN_LITE_LIMITS not found")
|
||||
const oldValue = lines.find((line) => line.startsWith("ZEN_LIMITS"))?.split("=")[1] ?? "{}"
|
||||
if (!oldValue) throw new Error("ZEN_LIMITS not found")
|
||||
|
||||
// store the prettified json to a temp file
|
||||
const filename = `lite-${Date.now()}.json`
|
||||
const filename = `limits-${Date.now()}.json`
|
||||
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
||||
await tempFile.write(JSON.stringify(JSON.parse(oldValue), null, 2))
|
||||
console.log("tempFile", tempFile.name)
|
||||
@ -22,7 +22,7 @@ console.log("tempFile", tempFile.name)
|
||||
// open temp file in vim and read the file on close
|
||||
await $`vim ${tempFile.name}`
|
||||
const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
|
||||
LiteData.validate(JSON.parse(newValue))
|
||||
Subscription.validate(JSON.parse(newValue))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_LITE_LIMITS ${newValue}`
|
||||
await $`bun sst secret set ZEN_LIMITS ${newValue}`
|
||||
@ -2,37 +2,15 @@ import { z } from "zod"
|
||||
import { fn } from "./util/fn"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { BlackPlans } from "./schema/billing.sql"
|
||||
import { Subscription } from "./subscription"
|
||||
|
||||
export namespace BlackData {
|
||||
const Schema = z.object({
|
||||
"200": z.object({
|
||||
fixedLimit: z.number().int(),
|
||||
rollingLimit: z.number().int(),
|
||||
rollingWindow: z.number().int(),
|
||||
}),
|
||||
"100": z.object({
|
||||
fixedLimit: z.number().int(),
|
||||
rollingLimit: z.number().int(),
|
||||
rollingWindow: z.number().int(),
|
||||
}),
|
||||
"20": z.object({
|
||||
fixedLimit: z.number().int(),
|
||||
rollingLimit: z.number().int(),
|
||||
rollingWindow: z.number().int(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const validate = fn(Schema, (input) => {
|
||||
return input
|
||||
})
|
||||
|
||||
export const getLimits = fn(
|
||||
z.object({
|
||||
plan: z.enum(BlackPlans),
|
||||
}),
|
||||
({ plan }) => {
|
||||
const json = JSON.parse(Resource.ZEN_BLACK_LIMITS.value)
|
||||
return Schema.parse(json)[plan]
|
||||
return Subscription.getLimits()["black"][plan]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@ -1,22 +1,11 @@
|
||||
import { z } from "zod"
|
||||
import { fn } from "./util/fn"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { Subscription } from "./subscription"
|
||||
|
||||
export namespace LiteData {
|
||||
const Schema = z.object({
|
||||
rollingLimit: z.number().int(),
|
||||
rollingWindow: z.number().int(),
|
||||
weeklyLimit: z.number().int(),
|
||||
monthlyLimit: z.number().int(),
|
||||
})
|
||||
|
||||
export const validate = fn(Schema, (input) => {
|
||||
return input
|
||||
})
|
||||
|
||||
export const getLimits = fn(z.void(), () => {
|
||||
const json = JSON.parse(Resource.ZEN_LITE_LIMITS.value)
|
||||
return Schema.parse(json)
|
||||
return Subscription.getLimits()["lite"]
|
||||
})
|
||||
|
||||
export const productID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.product)
|
||||
|
||||
@ -9,24 +9,7 @@ import { Resource } from "@opencode-ai/console-resource"
|
||||
|
||||
export namespace ZenData {
|
||||
const FormatSchema = z.enum(["anthropic", "google", "openai", "oa-compat"])
|
||||
const TrialSchema = z.object({
|
||||
provider: z.string(),
|
||||
limits: z.array(
|
||||
z.object({
|
||||
limit: z.number(),
|
||||
client: z.enum(["cli", "desktop"]).optional(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
const RateLimitSchema = z.object({
|
||||
period: z.enum(["day", "rolling"]),
|
||||
value: z.number().int(),
|
||||
checkHeader: z.string().optional(),
|
||||
fallbackValue: z.number().int().optional(),
|
||||
})
|
||||
export type Format = z.infer<typeof FormatSchema>
|
||||
export type Trial = z.infer<typeof TrialSchema>
|
||||
export type RateLimit = z.infer<typeof RateLimitSchema>
|
||||
|
||||
const ModelCostSchema = z.object({
|
||||
input: z.number(),
|
||||
@ -43,8 +26,7 @@ export namespace ZenData {
|
||||
allowAnonymous: z.boolean().optional(),
|
||||
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
|
||||
stickyProvider: z.enum(["strict", "prefer"]).optional(),
|
||||
trial: TrialSchema.optional(),
|
||||
rateLimit: RateLimitSchema.optional(),
|
||||
trialProvider: z.string().optional(),
|
||||
fallbackProvider: z.string().optional(),
|
||||
providers: z.array(
|
||||
z.object({
|
||||
@ -63,19 +45,12 @@ export namespace ZenData {
|
||||
format: FormatSchema.optional(),
|
||||
headerMappings: z.record(z.string(), z.string()).optional(),
|
||||
payloadModifier: z.record(z.string(), z.any()).optional(),
|
||||
family: z.string().optional(),
|
||||
})
|
||||
|
||||
const ProviderFamilySchema = z.object({
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
responseModifier: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
|
||||
const ModelsSchema = z.object({
|
||||
models: z.record(z.string(), z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))])),
|
||||
liteModels: z.record(z.string(), ModelSchema),
|
||||
providers: z.record(z.string(), ProviderSchema),
|
||||
providerFamilies: z.record(z.string(), ProviderFamilySchema),
|
||||
})
|
||||
|
||||
export const validate = fn(ModelsSchema, (input) => {
|
||||
@ -115,15 +90,10 @@ export namespace ZenData {
|
||||
Resource.ZEN_MODELS29.value +
|
||||
Resource.ZEN_MODELS30.value,
|
||||
)
|
||||
const { models, liteModels, providers, providerFamilies } = ModelsSchema.parse(json)
|
||||
const { models, liteModels, providers } = ModelsSchema.parse(json)
|
||||
return {
|
||||
models: modelList === "lite" ? liteModels : models,
|
||||
providers: Object.fromEntries(
|
||||
Object.entries(providers).map(([id, provider]) => [
|
||||
id,
|
||||
{ ...provider, ...(provider.family ? providerFamilies[provider.family] : {}) },
|
||||
]),
|
||||
),
|
||||
providers,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -2,8 +2,54 @@ import { z } from "zod"
|
||||
import { fn } from "./util/fn"
|
||||
import { centsToMicroCents } from "./util/price"
|
||||
import { getWeekBounds, getMonthlyBounds } from "./util/date"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
|
||||
export namespace Subscription {
|
||||
const LimitsSchema = z.object({
|
||||
free: z.object({
|
||||
promoTokens: z.number().int(),
|
||||
dailyRequests: z.number().int(),
|
||||
checkHeader: z.string(),
|
||||
fallbackValue: z.number().int(),
|
||||
}),
|
||||
lite: z.object({
|
||||
rollingLimit: z.number().int(),
|
||||
rollingWindow: z.number().int(),
|
||||
weeklyLimit: z.number().int(),
|
||||
monthlyLimit: z.number().int(),
|
||||
}),
|
||||
black: z.object({
|
||||
"20": z.object({
|
||||
fixedLimit: z.number().int(),
|
||||
rollingLimit: z.number().int(),
|
||||
rollingWindow: z.number().int(),
|
||||
}),
|
||||
"100": z.object({
|
||||
fixedLimit: z.number().int(),
|
||||
rollingLimit: z.number().int(),
|
||||
rollingWindow: z.number().int(),
|
||||
}),
|
||||
"200": z.object({
|
||||
fixedLimit: z.number().int(),
|
||||
rollingLimit: z.number().int(),
|
||||
rollingWindow: z.number().int(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
export const validate = fn(LimitsSchema, (input) => {
|
||||
return input
|
||||
})
|
||||
|
||||
export const getLimits = fn(z.void(), () => {
|
||||
const json = JSON.parse(Resource.ZEN_LIMITS.value)
|
||||
return LimitsSchema.parse(json)
|
||||
})
|
||||
|
||||
export const getFreeLimits = fn(z.void(), () => {
|
||||
return getLimits()["free"]
|
||||
})
|
||||
|
||||
export const analyzeRollingUsage = fn(
|
||||
z.object({
|
||||
limit: z.number().int(),
|
||||
|
||||
6
packages/console/core/sst-env.d.ts
vendored
6
packages/console/core/sst-env.d.ts
vendored
@ -119,10 +119,6 @@ declare module "sst" {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"ZEN_BLACK_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_BLACK_PRICE": {
|
||||
"plan100": string
|
||||
"plan20": string
|
||||
@ -130,7 +126,7 @@ declare module "sst" {
|
||||
"product": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
"ZEN_LITE_LIMITS": {
|
||||
"ZEN_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
|
||||
6
packages/console/function/sst-env.d.ts
vendored
6
packages/console/function/sst-env.d.ts
vendored
@ -119,10 +119,6 @@ declare module "sst" {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"ZEN_BLACK_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_BLACK_PRICE": {
|
||||
"plan100": string
|
||||
"plan20": string
|
||||
@ -130,7 +126,7 @@ declare module "sst" {
|
||||
"product": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
"ZEN_LITE_LIMITS": {
|
||||
"ZEN_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
|
||||
6
packages/console/resource/sst-env.d.ts
vendored
6
packages/console/resource/sst-env.d.ts
vendored
@ -119,10 +119,6 @@ declare module "sst" {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"ZEN_BLACK_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_BLACK_PRICE": {
|
||||
"plan100": string
|
||||
"plan20": string
|
||||
@ -130,7 +126,7 @@ declare module "sst" {
|
||||
"product": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
"ZEN_LITE_LIMITS": {
|
||||
"ZEN_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
|
||||
6
packages/enterprise/sst-env.d.ts
vendored
6
packages/enterprise/sst-env.d.ts
vendored
@ -119,10 +119,6 @@ declare module "sst" {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"ZEN_BLACK_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_BLACK_PRICE": {
|
||||
"plan100": string
|
||||
"plan20": string
|
||||
@ -130,7 +126,7 @@ declare module "sst" {
|
||||
"product": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
"ZEN_LITE_LIMITS": {
|
||||
"ZEN_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
|
||||
6
packages/function/sst-env.d.ts
vendored
6
packages/function/sst-env.d.ts
vendored
@ -119,10 +119,6 @@ declare module "sst" {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"ZEN_BLACK_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_BLACK_PRICE": {
|
||||
"plan100": string
|
||||
"plan20": string
|
||||
@ -130,7 +126,7 @@ declare module "sst" {
|
||||
"product": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
"ZEN_LITE_LIMITS": {
|
||||
"ZEN_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
|
||||
0
packages/sdk/js/openapi.json
Normal file
0
packages/sdk/js/openapi.json
Normal file
10
packages/storybook/sst-env.d.ts
vendored
Normal file
10
packages/storybook/sst-env.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
6
sst-env.d.ts
vendored
6
sst-env.d.ts
vendored
@ -145,10 +145,6 @@ declare module "sst" {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"ZEN_BLACK_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_BLACK_PRICE": {
|
||||
"plan100": string
|
||||
"plan20": string
|
||||
@ -156,7 +152,7 @@ declare module "sst" {
|
||||
"product": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
"ZEN_LITE_LIMITS": {
|
||||
"ZEN_LIMITS": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user