Skip to content

Commit c98363e

Browse files
committed
Allow passing a custom timeout to opencode run
Some models, like `gpt-5-pro`, can think for way longer than the default 5 minutes timeout. This PR allows the customization of this parameter.
1 parent dfebf40 commit c98363e

File tree

3 files changed

+33
-9
lines changed

3 files changed

+33
-9
lines changed

packages/opencode/src/cli/cmd/run.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ export const RunCommand = cmd({
7878
array: true,
7979
describe: "file(s) to attach to message",
8080
})
81+
.option("timeout", {
82+
type: "number",
83+
describe: "timeout in milliseconds for each AI request (default: 300000 ms, 5 mins)",
84+
})
8185
},
8286
handler: async (args) => {
8387
let message = args.message.join(" ")
@@ -268,6 +272,7 @@ export const RunCommand = cmd({
268272
model: providerID + "/" + modelID,
269273
command: args.command,
270274
arguments: message,
275+
timeout: args.timeout,
271276
})
272277
}
273278
return await SessionPrompt.prompt({
@@ -286,6 +291,7 @@ export const RunCommand = cmd({
286291
text: message,
287292
},
288293
],
294+
timeout: args.timeout,
289295
})
290296
})()
291297
if (errorMsg) process.exit(1)

packages/opencode/src/provider/provider.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ export namespace Provider {
398398
return state().then((state) => state.providers)
399399
}
400400

401-
async function getSDK(provider: ModelsDev.Provider, model: ModelsDev.Model) {
401+
async function getSDK(provider: ModelsDev.Provider, model: ModelsDev.Model, runtimeOptions?: Record<string, any>) {
402402
return (async () => {
403403
using _ = log.time("getSDK", {
404404
providerID: provider.id,
@@ -422,14 +422,15 @@ export namespace Provider {
422422
const modPath =
423423
provider.id === "google-vertex-anthropic" ? `${installedPath}/dist/anthropic/index.mjs` : installedPath
424424
const mod = await import(modPath)
425-
if (options["timeout"] !== undefined && options["timeout"] !== null) {
425+
const timeout = runtimeOptions?.timeout ?? options["timeout"]
426+
if (timeout !== undefined && timeout !== null) {
426427
// Only override fetch if user explicitly sets timeout
427428
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
428429
const { signal, ...rest } = init ?? {}
429430

430431
const signals: AbortSignal[] = []
431432
if (signal) signals.push(signal)
432-
if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
433+
if (timeout !== false) signals.push(AbortSignal.timeout(timeout))
433434

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

@@ -457,10 +458,16 @@ export namespace Provider {
457458
return state().then((s) => s.providers[providerID])
458459
}
459460

460-
export async function getModel(providerID: string, modelID: string) {
461-
const key = `${providerID}/${modelID}`
461+
export async function getModel(providerID: string, modelID: string, runtimeOptions?: Record<string, any>) {
462462
const s = await state()
463-
if (s.models.has(key)) return s.models.get(key)!
463+
464+
// Include runtime options in cache key
465+
// Runtime options are only used from `opencode run` command
466+
const cacheKey = runtimeOptions
467+
? `${providerID}/${modelID}:${Bun.hash.xxHash32(JSON.stringify(runtimeOptions))}`
468+
: `${providerID}/${modelID}`
469+
470+
if (s.models.has(cacheKey)) return s.models.get(cacheKey)!
464471

465472
log.info("getModel", {
466473
providerID,
@@ -471,7 +478,7 @@ export namespace Provider {
471478
if (!provider) throw new ModelNotFoundError({ providerID, modelID })
472479
const info = provider.info.models[modelID]
473480
if (!info) throw new ModelNotFoundError({ providerID, modelID })
474-
const sdk = await getSDK(provider.info, info)
481+
const sdk = await getSDK(provider.info, info, runtimeOptions)
475482

476483
try {
477484
const keyReal = `${providerID}/${modelID}`
@@ -480,7 +487,7 @@ export namespace Provider {
480487
? await provider.getModel(sdk, realID, provider.options)
481488
: sdk.languageModel(realID)
482489
log.info("found", { providerID, modelID })
483-
s.models.set(key, {
490+
s.models.set(cacheKey, {
484491
providerID,
485492
modelID,
486493
info,

packages/opencode/src/session/prompt.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export namespace SessionPrompt {
9797
noReply: z.boolean().optional(),
9898
system: z.string().optional(),
9999
tools: z.record(z.string(), z.boolean()).optional(),
100+
timeout: z.number().int().positive().optional(),
100101
parts: z.array(
101102
z.discriminatedUnion("type", [
102103
MessageV2.TextPart.omit({
@@ -158,11 +159,19 @@ export namespace SessionPrompt {
158159
state().queued.set(input.sessionID, queue)
159160
})
160161
}
162+
163+
// Build runtime options
164+
let runtimeOptions: Record<string, unknown> | undefined = undefined
165+
if (input.timeout) {
166+
runtimeOptions ??= {}
167+
runtimeOptions.timeout = input.timeout
168+
}
169+
161170
const agent = await Agent.get(input.agent ?? "build")
162171
const model = await resolveModel({
163172
agent,
164173
model: input.model,
165-
}).then((x) => Provider.getModel(x.providerID, x.modelID))
174+
}).then((x) => Provider.getModel(x.providerID, x.modelID, runtimeOptions))
166175

167176
using abort = lock(input.sessionID)
168177

@@ -1467,6 +1476,7 @@ export namespace SessionPrompt {
14671476
model: z.string().optional(),
14681477
arguments: z.string(),
14691478
command: z.string(),
1479+
timeout: z.number().int().positive().optional(),
14701480
})
14711481
export type CommandInput = z.infer<typeof CommandInput>
14721482
const bashRegex = /!`([^`]+)`/g
@@ -1679,6 +1689,7 @@ export namespace SessionPrompt {
16791689
model,
16801690
agent: agentName,
16811691
parts,
1692+
timeout: input.timeout,
16821693
})
16831694
}
16841695

0 commit comments

Comments
 (0)