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
13 changes: 13 additions & 0 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,17 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
return {}
}

const REASONING_CONNECT_TIMEOUT_MS = 120_000

/**
* Returns watchdog timeout overrides for reasoning-capable models.
* Apply at every llm.stream() call site to keep coverage uniform.
*/
export function streamTimeouts(model: Provider.Model): { connectTimeoutMs?: number } {
if (!model.capabilities.reasoning) return {}
return { connectTimeoutMs: REASONING_CONNECT_TIMEOUT_MS }
}

export function options(input: {
model: Provider.Model
sessionID: string
Expand Down Expand Up @@ -1497,6 +1508,7 @@ const ProviderTransformTemperatureValue = temperature
const ProviderTransformTopPValue = topP
const ProviderTransformTopKValue = topK
const ProviderTransformSmallOptionsValue = smallOptions
const ProviderTransformStreamTimeoutsValue = streamTimeouts

export namespace ProviderTransform {
export const OUTPUT_TOKEN_MAX = ProviderTransformOutputTokenMaxValue
Expand All @@ -1512,4 +1524,5 @@ export namespace ProviderTransform {
export const topP = ProviderTransformTopPValue
export const topK = ProviderTransformTopKValue
export const smallOptions = ProviderTransformSmallOptionsValue
export const streamTimeouts = ProviderTransformStreamTimeoutsValue
}
7 changes: 6 additions & 1 deletion packages/opencode/src/session/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { SessionSummary } from "./summary"
import { SessionDiagnostics } from "./diagnostics"
import { classifyToolFailure } from "./tool-failure"
import type { Provider } from "@/provider"
import { ProviderTransform } from "@/provider"
import { Question } from "@/question"
import { errorMessage } from "@/util/error"
import { Log } from "@opencode-ai/core/util/log"
Expand Down Expand Up @@ -949,7 +950,11 @@ export const layer: Layer.Layer<
yield* Effect.gen(function* () {
ctx.currentText = undefined
ctx.reasoningMap = {}
const stream = llm.stream({ ...streamInput, trace: ctx.trace })
const stream = llm.stream({
...ProviderTransform.streamTimeouts(streamInput.model),
...streamInput,
trace: ctx.trace,
})

yield* stream.pipe(
Stream.tap((event) => handleEvent(event)),
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ export const layer = Layer.effect(
titleGenerationProgress.set(input.session.id, { startedAt })
const titleExit = yield* llm
.stream({
...ProviderTransform.streamTimeouts(mdl),
agent: ag,
user: firstInfo,
system: [],
Expand Down
58 changes: 58 additions & 0 deletions packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test"
import { ProviderTransform } from "../../src/provider"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { LLM } from "../../src/session/llm"

describe("ProviderTransform.options - setCacheKey", () => {
const sessionID = "test-session-123"
Expand Down Expand Up @@ -4720,3 +4721,60 @@ describe("ProviderTransform.variants", () => {
})
})
})

describe("ProviderTransform.streamTimeouts", () => {
const baseModel = {
id: "test/test-model",
providerID: "test",
api: {
id: "test-model",
url: "https://api.test.com",
npm: "@ai-sdk/openai",
},
name: "Test Model",
capabilities: {
temperature: true,
reasoning: false,
attachment: false,
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: 100_000, output: 8_192 },
status: "active",
options: {},
headers: {},
release_date: "2024-01-01",
} as any

const reasoningModel = {
...baseModel,
capabilities: { ...baseModel.capabilities, reasoning: true },
}
const nonReasoningModel = baseModel

// Floor of 90_000ms guards against a regression like 31s that would still
// exceed the 30s default but defeat the purpose of the widened ceiling.
// 90s is the lowest value considered for reasoning models in #755.
test("policy floor: reasoning model connect timeout meets minimum ceiling", () => {
const result = ProviderTransform.streamTimeouts(reasoningModel)
expect(result.connectTimeoutMs).toBeDefined()
expect(result.connectTimeoutMs!).toBeGreaterThan(LLM.CONNECT_STREAM_TIMEOUT_MS)
expect(result.connectTimeoutMs!).toBeGreaterThanOrEqual(90_000)
})

test("routing contract: reasoning model emits override, non-reasoning model emits empty", () => {
expect(ProviderTransform.streamTimeouts(reasoningModel).connectTimeoutMs).toBeGreaterThan(0)
expect(ProviderTransform.streamTimeouts(nonReasoningModel).connectTimeoutMs).toBeUndefined()
})

// Mirrors the helper-first spread order at the production call sites so a
// caller-provided override on StreamInput still wins after the helper is applied.
test("caller override precedence: explicit connectTimeoutMs wins over helper", () => {
const callerInput = { connectTimeoutMs: 5_000 }
const merged = { ...ProviderTransform.streamTimeouts(reasoningModel), ...callerInput }
expect(merged.connectTimeoutMs).toBe(5_000)
})
})