Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions packages/opencode/src/cli/cmd/stats.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fs from "fs"
import path from "path"
import type { Argv } from "yargs"
import { cmd } from "./cmd"
import { Session } from "../../session"
Expand All @@ -7,6 +9,7 @@ import { SessionTable } from "../../session/session.sql"
import { Project } from "../../project"
import { Instance } from "../../project/instance"
import { AppRuntime } from "@/effect/app-runtime"
import { Global } from "../../global"

interface SessionStats {
totalSessions: number
Expand Down Expand Up @@ -80,6 +83,7 @@ export const StatsCommand = cmd({
}

displayStats(stats, args.tools, modelLimit)
displayRateLimits()
})
},
})
Expand Down Expand Up @@ -411,3 +415,52 @@ function formatNumber(num: number): string {
}
return num.toString()
}

function displayRateLimits() {
const width = 56
function renderRow(label: string, value: string): string {
const availableWidth = width - 1
const paddingNeeded = availableWidth - label.length - value.length
const padding = Math.max(0, paddingNeeded)
return `│${label}${" ".repeat(padding)}${value} │`
}

const jsonPath = path.join(Global.Path.config, "opencode.json")
let data: any
try {
data = JSON.parse(fs.readFileSync(jsonPath, "utf8"))
} catch {
return
}
const providers = data?.provider
if (!providers || typeof providers !== "object") return

const rows: Array<{ id: string; perMinute?: number; perDay?: number; tokensPerMinute?: number; tokensPerDay?: number }> = []
for (const [id, cfg] of Object.entries<any>(providers)) {
const rl = cfg?.options?.rateLimit
if (!rl) continue
rows.push({
id,
perMinute: rl.perMinute,
perDay: rl.perDay,
tokensPerMinute: rl.tokensPerMinute,
tokensPerDay: rl.tokensPerDay,
})
}
if (rows.length === 0) return

console.log("┌────────────────────────────────────────────────────────┐")
console.log("│ RATE LIMITS (learned/set) │")
console.log("├────────────────────────────────────────────────────────┤")
for (const r of rows) {
console.log(`│ ${r.id.padEnd(54)} │`)
if (r.perMinute !== undefined) console.log(renderRow(" Requests/min", formatNumber(r.perMinute)))
if (r.perDay !== undefined) console.log(renderRow(" Requests/day", formatNumber(r.perDay)))
if (r.tokensPerMinute !== undefined) console.log(renderRow(" Tokens/min", formatNumber(r.tokensPerMinute)))
if (r.tokensPerDay !== undefined) console.log(renderRow(" Tokens/day", formatNumber(r.tokensPerDay)))
console.log("├────────────────────────────────────────────────────────┤")
}
process.stdout.write("\x1B[1A")
console.log("└────────────────────────────────────────────────────────┘")
console.log()
}
8 changes: 7 additions & 1 deletion packages/opencode/src/config/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,15 @@ export class Info extends Schema.Class<Info>("ProviderConfig")({
perDay: Schema.optional(PositiveInt).annotate({
description: "Learned or user-set request limit per 24 hours.",
}),
tokensPerMinute: Schema.optional(PositiveInt).annotate({
description: "Learned or user-set token limit per 60 seconds.",
}),
tokensPerDay: Schema.optional(PositiveInt).annotate({
description: "Learned or user-set token limit per 24 hours.",
}),
}).annotate({
description:
"Request-rate limits for this provider. Populated automatically the first time a 429 response is received, or can be set manually.",
"Request- and token-rate limits for this provider. Populated automatically the first time a 429 response is received, or can be set manually.",
}),
),
}),
Expand Down
12 changes: 9 additions & 3 deletions packages/opencode/src/provider/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,20 @@ export function parseAPICallError(input: { providerID: ProviderID; error: APICal
}

const metadata = input.error.url ? { url: input.error.url } : undefined
if (input.error.statusCode === 429) {
const is429 = input.error.statusCode === 429
if (is429) {
RateLimit.onRateLimitError(input.providerID)
}
const friendlyMessage = is429 ? `Rate limit hit on ${input.providerID} — retrying` : m
return {
type: "api_error",
message: m,
message: friendlyMessage,
statusCode: input.error.statusCode,
isRetryable: input.providerID.startsWith("openai") ? isOpenAiErrorRetryable(input.error) : input.error.isRetryable,
isRetryable: is429
? true
: input.providerID.startsWith("openai")
? isOpenAiErrorRetryable(input.error)
: input.error.isRetryable,
responseHeaders: input.error.responseHeaders,
responseBody: input.error.responseBody,
metadata,
Expand Down
21 changes: 19 additions & 2 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { withStatics } from "@/util/schema"

import * as ProviderTransform from "./transform"
import { ModelID, ProviderID } from "./schema"
import { RateLimit } from "./rate-limit"
import { RateLimit, RateLimitError, formatGateMessage } from "./rate-limit"

const log = Log.create({ service: "provider" })

Expand Down Expand Up @@ -1443,6 +1443,11 @@ const layer: Layer.Layer<
const chunkTimeout = options["chunkTimeout"]
delete options["chunkTimeout"]

if (options["rateLimit"] && typeof options["rateLimit"] === "object") {
RateLimit.configure(model.providerID, options["rateLimit"] as any)
}
delete options["rateLimit"]

options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
const fetchFn = customFetch ?? fetch
const opts = init ?? {}
Expand Down Expand Up @@ -1472,7 +1477,19 @@ const layer: Layer.Layer<
}
}

RateLimit.tick(model.providerID)
const estimate = RateLimit.estimateRequestTokens(opts.body)
const gate = RateLimit.check(model.providerID, estimate)
if (!gate.ok) {
throw new RateLimitError({
providerID: model.providerID,
reason: gate.reason,
limit: gate.limit,
current: gate.current,
resetAt: gate.resetAt,
message: formatGateMessage(model.providerID, gate),
})
}
RateLimit.tick(model.providerID, estimate)
const res = await fetchFn(input, {
...opts,
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
Expand Down
Loading