Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions STATS.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,10 @@
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,320 (+11,246) | 630,885 (+11,195) | 1,317,205 (+22,441) |
| 2025-11-07 | 696,702 (+10,382) | 642,146 (+11,261) | 1,338,848 (+21,643) |
| 2025-11-08 | 706,079 (+9,377) | 653,489 (+11,343) | 1,359,568 (+20,720) |
| 2025-11-09 | 713,521 (+7,442) | 660,459 (+6,970) | 1,373,980 (+14,412) |
| 2025-11-10 | 722,366 (+8,845) | 668,225 (+7,766) | 1,390,591 (+16,611) |
| 2025-11-11 | 729,814 (+7,448) | 677,501 (+9,276) | 1,407,315 (+16,724) |
| 2025-11-12 | 740,245 (+10,431) | 686,454 (+8,953) | 1,426,699 (+19,384) |
6 changes: 6 additions & 0 deletions packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export const RunCommand = cmd({
type: "string",
describe: "title for the session (uses truncated prompt if no value provided)",
})
.option("timeout", {
type: "string",
describe: "timeout for each AI request (e.g., '60s', '2m', '1h', '500ms', default: '5m')",
})
},
handler: async (args) => {
let message = args.message.join(" ")
Expand Down Expand Up @@ -313,6 +317,7 @@ export const RunCommand = cmd({
model: providerID + "/" + modelID,
command: args.command,
arguments: message,
timeout: args.timeout,
})
}
return await SessionPrompt.prompt({
Expand All @@ -331,6 +336,7 @@ export const RunCommand = cmd({
text: message,
},
],
timeout: args.timeout,
})
})()
if (errorMsg) process.exit(1)
Expand Down
23 changes: 15 additions & 8 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ export namespace Provider {
return state().then((state) => state.providers)
}

async function getSDK(provider: ModelsDev.Provider, model: ModelsDev.Model) {
async function getSDK(provider: ModelsDev.Provider, model: ModelsDev.Model, runtimeOptions?: Record<string, any>) {
return (async () => {
using _ = log.time("getSDK", {
providerID: provider.id,
Expand All @@ -499,15 +499,16 @@ export namespace Provider {
? `${installedPath}/dist/anthropic/index.mjs`
: installedPath
const mod = await import(modPath)
if (options["timeout"] !== undefined && options["timeout"] !== null) {
const timeout = runtimeOptions?.timeout ?? options["timeout"]
if (timeout !== undefined && timeout !== null) {
// Preserve custom fetch if it exists, wrap it with timeout logic
const customFetch = options["fetch"]
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
const { signal, ...rest } = init ?? {}

const signals: AbortSignal[] = []
if (signal) signals.push(signal)
if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
if (timeout !== false) signals.push(AbortSignal.timeout(timeout))

const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]

Expand Down Expand Up @@ -536,10 +537,16 @@ export namespace Provider {
return state().then((s) => s.providers[providerID])
}

export async function getModel(providerID: string, modelID: string) {
const key = `${providerID}/${modelID}`
export async function getModel(providerID: string, modelID: string, runtimeOptions?: Record<string, any>) {
const s = await state()
if (s.models.has(key)) return s.models.get(key)!

// Include runtime options in cache key
// Runtime options are only used from `opencode run` command
const cacheKey = runtimeOptions
? `${providerID}/${modelID}:${Bun.hash.xxHash32(JSON.stringify(runtimeOptions))}`
: `${providerID}/${modelID}`

if (s.models.has(cacheKey)) return s.models.get(cacheKey)!

log.info("getModel", {
providerID,
Expand All @@ -550,7 +557,7 @@ export namespace Provider {
if (!provider) throw new ModelNotFoundError({ providerID, modelID })
const info = provider.info.models[modelID]
if (!info) throw new ModelNotFoundError({ providerID, modelID })
const sdk = await getSDK(provider.info, info)
const sdk = await getSDK(provider.info, info, runtimeOptions)

try {
const keyReal = `${providerID}/${modelID}`
Expand All @@ -559,7 +566,7 @@ export namespace Provider {
? await provider.getModel(sdk, realID, provider.options)
: sdk.languageModel(realID)
log.info("found", { providerID, modelID })
s.models.set(key, {
s.models.set(cacheKey, {
providerID,
modelID,
info,
Expand Down
13 changes: 12 additions & 1 deletion packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { ConfigMarkdown } from "../config/markdown"
import { SessionSummary } from "./summary"
import { Config } from "@/config/config"
import { NamedError } from "@/util/error"
import { parseTimeout } from "@/util/timeout"

export namespace SessionPrompt {
const log = Log.create({ service: "session.prompt" })
Expand Down Expand Up @@ -109,6 +110,7 @@ export namespace SessionPrompt {
noReply: z.boolean().optional(),
system: z.string().optional(),
tools: z.record(z.string(), z.boolean()).optional(),
timeout: z.string().optional(),
parts: z.array(
z.discriminatedUnion("type", [
MessageV2.TextPart.omit({
Expand Down Expand Up @@ -170,11 +172,19 @@ export namespace SessionPrompt {
state().queued.set(input.sessionID, queue)
})
}

// Build runtime options
let runtimeOptions: Record<string, unknown> | undefined = undefined
if (input.timeout) {
runtimeOptions ??= {}
runtimeOptions.timeout = parseTimeout(input.timeout)
}

const agent = await Agent.get(input.agent ?? "build")
const model = await resolveModel({
agent,
model: input.model,
}).then((x) => Provider.getModel(x.providerID, x.modelID))
}).then((x) => Provider.getModel(x.providerID, x.modelID, runtimeOptions))

using abort = lock(input.sessionID)

Expand Down Expand Up @@ -1568,6 +1578,7 @@ export namespace SessionPrompt {
model: z.string().optional(),
arguments: z.string(),
command: z.string(),
timeout: z.string().optional(),
})
export type CommandInput = z.infer<typeof CommandInput>
const bashRegex = /!`([^`]+)`/g
Expand Down
25 changes: 25 additions & 0 deletions packages/opencode/src/util/timeout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,28 @@ export function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
}),
])
}

export function parseTimeout(timeout: string): number {
const match = timeout.match(/^(\d+(?:\.\d+)?)\s*(h|m|s|ms)?$/)
if (!match) {
throw new Error(
`Invalid timeout format (${timeout}). Expected format: "60s", "2m", "1h", "500ms".`,
)
}

const value = parseFloat(match[1])
const unit = match[2] || "ms"

switch (unit) {
case "h":
return value * 60 * 60 * 1000
case "m":
return value * 60 * 1000
case "s":
return value * 1000
case "ms":
return value
default:
throw new Error(`Invalid timeout unit (${unit}). Expected h, m, s, or ms.`)
}
}