Skip to content

Commit 4408774

Browse files
authored
Merge branch 'dev' into kit/residual-facade-cleanup
2 parents e750e32 + 87b2a9d commit 4408774

13 files changed

Lines changed: 7636 additions & 36 deletions

File tree

packages/app/src/context/global-sync/bootstrap.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -122,20 +122,21 @@ export async function bootstrapGlobal(input: {
122122
}),
123123
),
124124
]
125-
126-
showErrors({
127-
errors: errors(await runAll(fast)),
128-
title: input.requestFailedTitle,
129-
translate: input.translate,
130-
formatMoreCount: input.formatMoreCount,
131-
})
125+
await runAll(fast)
126+
// showErrors({
127+
// errors: errors(await runAll(fast)),
128+
// title: input.requestFailedTitle,
129+
// translate: input.translate,
130+
// formatMoreCount: input.formatMoreCount,
131+
// })
132132
await waitForPaint()
133-
showErrors({
134-
errors: errors(await runAll(slow)),
135-
title: input.requestFailedTitle,
136-
translate: input.translate,
137-
formatMoreCount: input.formatMoreCount,
138-
})
133+
await runAll(slow)
134+
// showErrors({
135+
// errors: errors(),
136+
// title: input.requestFailedTitle,
137+
// translate: input.translate,
138+
// formatMoreCount: input.formatMoreCount,
139+
// })
139140
input.setGlobalStore("ready", true)
140141
}
141142

packages/console/app/src/routes/zen/util/error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ class LimitError extends Error {
1111
this.retryAfter = retryAfter
1212
}
1313
}
14+
export class RateLimitError extends LimitError {}
1415
export class FreeUsageLimitError extends LimitError {}
1516
export class SubscriptionUsageLimitError extends LimitError {}

packages/console/app/src/routes/zen/util/handler.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
MonthlyLimitError,
2222
UserLimitError,
2323
ModelError,
24+
RateLimitError,
2425
FreeUsageLimitError,
2526
SubscriptionUsageLimitError,
2627
} from "./error"
@@ -35,7 +36,8 @@ import { anthropicHelper } from "./provider/anthropic"
3536
import { googleHelper } from "./provider/google"
3637
import { openaiHelper } from "./provider/openai"
3738
import { oaCompatHelper } from "./provider/openai-compatible"
38-
import { createRateLimiter } from "./rateLimiter"
39+
import { createRateLimiter as createIpRateLimiter } from "./ipRateLimiter"
40+
import { createRateLimiter as createKeyRateLimiter } from "./keyRateLimiter"
3941
import { createDataDumper } from "./dataDumper"
4042
import { createTrialLimiter } from "./trialLimiter"
4143
import { createStickyTracker } from "./stickyProviderTracker"
@@ -92,6 +94,8 @@ export async function handler(
9294
const isStream = opts.parseIsStream(url, body)
9395
const rawIp = input.request.headers.get("x-real-ip") ?? ""
9496
const ip = rawIp.includes(":") ? rawIp.split(":").slice(0, 4).join(":") : rawIp
97+
const rawZenApiKey = opts.parseApiKey(input.request.headers)
98+
const zenApiKey = rawZenApiKey === "public" ? undefined : rawZenApiKey
9599
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
96100
const requestId = input.request.headers.get("x-opencode-request") ?? ""
97101
const projectId = input.request.headers.get("x-opencode-project") ?? ""
@@ -108,17 +112,13 @@ export async function handler(
108112
const dataDumper = createDataDumper(sessionId, requestId, projectId)
109113
const trialLimiter = createTrialLimiter(modelInfo.trialProvider, ip)
110114
const trialProviders = await trialLimiter?.check()
111-
const rateLimiter = createRateLimiter(
112-
modelInfo.id,
113-
modelInfo.allowAnonymous,
114-
modelInfo.rateLimit,
115-
ip,
116-
input.request,
117-
)
115+
const rateLimiter = modelInfo.allowAnonymous
116+
? createIpRateLimiter(modelInfo.id, modelInfo.rateLimit, ip, input.request)
117+
: createKeyRateLimiter(modelInfo.id, zenApiKey, input.request)
118118
await rateLimiter?.check()
119119
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
120120
const stickyProvider = await stickyTracker?.get()
121-
const authInfo = await authenticate(modelInfo)
121+
const authInfo = await authenticate(modelInfo, zenApiKey)
122122
const billingSource = validateBilling(authInfo, modelInfo)
123123
logger.metric({ source: billingSource })
124124

@@ -363,7 +363,11 @@ export async function handler(
363363
{ status: 401 },
364364
)
365365

366-
if (error instanceof FreeUsageLimitError || error instanceof SubscriptionUsageLimitError) {
366+
if (
367+
error instanceof RateLimitError ||
368+
error instanceof FreeUsageLimitError ||
369+
error instanceof SubscriptionUsageLimitError
370+
) {
367371
const headers = new Headers()
368372
if (error.retryAfter) {
369373
headers.set("retry-after", String(error.retryAfter))
@@ -492,9 +496,8 @@ export async function handler(
492496
}
493497
}
494498

495-
async function authenticate(modelInfo: ModelInfo) {
496-
const apiKey = opts.parseApiKey(input.request.headers)
497-
if (!apiKey || apiKey === "public") {
499+
async function authenticate(modelInfo: ModelInfo, zenApiKey?: string) {
500+
if (!zenApiKey) {
498501
if (modelInfo.allowAnonymous) return
499502
throw new AuthError(t("zen.api.error.missingApiKey"))
500503
}
@@ -573,7 +576,7 @@ export async function handler(
573576
isNull(LiteTable.timeDeleted),
574577
),
575578
)
576-
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
579+
.where(and(eq(KeyTable.key, zenApiKey), isNull(KeyTable.timeDeleted)))
577580
.then((rows) => rows[0]),
578581
)
579582

packages/console/app/src/routes/zen/util/rateLimiter.ts renamed to packages/console/app/src/routes/zen/util/ipRateLimiter.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,7 @@ import { i18n } from "~/i18n"
66
import { localeFromRequest } from "~/lib/language"
77
import { Subscription } from "@opencode-ai/console-core/subscription.js"
88

9-
export function createRateLimiter(
10-
modelId: string,
11-
allowAnonymous: boolean | undefined,
12-
rateLimit: number | undefined,
13-
rawIp: string,
14-
request: Request,
15-
) {
16-
if (!allowAnonymous) return
9+
export function createRateLimiter(modelId: string, rateLimit: number | undefined, rawIp: string, request: Request) {
1710
const dict = i18n(localeFromRequest(request))
1811

1912
const limits = Subscription.getFreeLimits()
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Database, eq, and, sql } from "@opencode-ai/console-core/drizzle/index.js"
2+
import { KeyRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
3+
import { RateLimitError } from "./error"
4+
import { i18n } from "~/i18n"
5+
import { localeFromRequest } from "~/lib/language"
6+
7+
export function createRateLimiter(modelId: string, zenApiKey: string | undefined, request: Request) {
8+
if (!zenApiKey) return
9+
const dict = i18n(localeFromRequest(request))
10+
11+
const LIMIT = 100
12+
const yyyyMMddHHmm = new Date(Date.now())
13+
.toISOString()
14+
.replace(/[^0-9]/g, "")
15+
.substring(0, 12)
16+
const interval = `${modelId.substring(0, 27)}-${yyyyMMddHHmm}`
17+
18+
return {
19+
check: async () => {
20+
const rows = await Database.use((tx) =>
21+
tx
22+
.select({ interval: KeyRateLimitTable.interval, count: KeyRateLimitTable.count })
23+
.from(KeyRateLimitTable)
24+
.where(and(eq(KeyRateLimitTable.key, zenApiKey), eq(KeyRateLimitTable.interval, interval))),
25+
).then((rows) => rows[0])
26+
const count = rows?.count ?? 0
27+
28+
if (count >= LIMIT) throw new RateLimitError(dict["zen.api.error.rateLimitExceeded"], 60)
29+
},
30+
track: async () => {
31+
await Database.use((tx) =>
32+
tx
33+
.insert(KeyRateLimitTable)
34+
.values({ key: zenApiKey, interval, count: 1 })
35+
.onDuplicateKeyUpdate({ set: { count: sql`${KeyRateLimitTable.count} + 1` } }),
36+
)
37+
},
38+
}
39+
}

packages/console/app/test/rateLimiter.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from "bun:test"
2-
import { getRetryAfterDay } from "../src/routes/zen/util/rateLimiter"
2+
import { getRetryAfterDay } from "../src/routes/zen/util/ipRateLimiter"
33

44
describe("getRetryAfterDay", () => {
55
test("returns full day at midnight UTC", () => {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE TABLE `key_rate_limit` (
2+
`key` varchar(255) NOT NULL,
3+
`interval` varchar(12) NOT NULL,
4+
`count` int NOT NULL,
5+
CONSTRAINT PRIMARY KEY(`key`,`interval`)
6+
);

0 commit comments

Comments
 (0)