Skip to content

Commit 290d419

Browse files
committed
refactor(opencode): simplify output cap
1 parent 589072e commit 290d419

4 files changed

Lines changed: 20 additions & 10 deletions

File tree

packages/opencode/src/session/processor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,8 @@ const layer = Layer.effect(
734734
SessionRetry.policy({
735735
provider: input.model.providerID,
736736
parse,
737+
// Only replace attempts that will be retried. Cloud intentionally
738+
// returns the terminal partial next to the truncation error.
737739
onRetry: (error) =>
738740
SessionV1.OutputLengthError.isInstance(error) ? resetOutputLimit() : Effect.void,
739741
set: (info) => {

packages/opencode/src/session/retry.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type RetryReason = "free_tier_limit" | "account_rate_limit" | (string & {
2323

2424
export type Retryable = {
2525
message: string
26+
maxAttempts?: number
2627
action?: {
2728
reason: RetryReason
2829
provider: string
@@ -37,7 +38,6 @@ export const RETRY_INITIAL_DELAY = 2000
3738
export const RETRY_BACKOFF_FACTOR = 2
3839
export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds
3940
export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout
40-
export const OUTPUT_LENGTH_MAX_RETRIES = 2
4141

4242
function cap(ms: number) {
4343
return Math.min(ms, RETRY_MAX_DELAY)
@@ -77,7 +77,9 @@ export function delay(attempt: number, error?: SessionV1.APIError) {
7777
}
7878

7979
export function retryable(error: Err, provider: string) {
80-
if (SessionV1.OutputLengthError.isInstance(error)) return { message: "Model hit its output limit" }
80+
if (SessionV1.OutputLengthError.isInstance(error)) {
81+
return { message: "Model hit its output limit", maxAttempts: 3 }
82+
}
8183
// context overflow errors should not be retried
8284
if (SessionV1.ContextOverflowError.isInstance(error)) return undefined
8385
if (SessionV1.APIError.isInstance(error)) {
@@ -191,15 +193,12 @@ export function policy(opts: {
191193
onRetry?: (error: Err) => Effect.Effect<void>
192194
set: (input: { attempt: number; message: string; action?: Retryable["action"]; next: number }) => Effect.Effect<void>
193195
}) {
194-
let outputLengthRetries = 0
195196
return Schedule.fromStepWithMetadata(
196197
Effect.succeed((meta: Schedule.InputMetadata<unknown>) => {
197198
const error = opts.parse(meta.input)
198199
const retry = retryable(error, opts.provider)
199200
if (!retry) return Cause.done(meta.attempt)
200-
if (SessionV1.OutputLengthError.isInstance(error) && ++outputLengthRetries > OUTPUT_LENGTH_MAX_RETRIES) {
201-
return Cause.done(meta.attempt)
202-
}
201+
if (retry.maxAttempts !== undefined && meta.attempt >= retry.maxAttempts) return Cause.done(meta.attempt)
203202
return Effect.gen(function* () {
204203
if (opts.onRetry) yield* opts.onRetry(error)
205204
const wait = delay(meta.attempt, SessionV1.APIError.isInstance(error) ? error : undefined)

packages/opencode/test/session/processor-effect.test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ const fragmentFailureEnv = LayerNode.compile(root, [...replacements, [LLM.node,
249249
const itFragmentFailure = testEffect(fragmentFailureEnv)
250250

251251
const outputRetryInputs: LLM.StreamInput[] = []
252+
const outputRetryUsage = {
253+
truncated: { input: 3, output: 5 },
254+
complete: { input: 7, output: 11 },
255+
} as const
252256
const outputRetryLLM = Layer.succeed(
253257
LLM.Service,
254258
LLM.Service.of({
@@ -263,7 +267,10 @@ const outputRetryLLM = Layer.succeed(
263267
LLMEvent.stepFinish({
264268
index: 0,
265269
reason: first ? "length" : "stop",
266-
usage: first ? { inputTokens: 3, outputTokens: 5 } : { inputTokens: 7, outputTokens: 11 },
270+
usage: {
271+
inputTokens: first ? outputRetryUsage.truncated.input : outputRetryUsage.complete.input,
272+
outputTokens: first ? outputRetryUsage.truncated.output : outputRetryUsage.complete.output,
273+
},
267274
}),
268275
LLMEvent.finish({ reason: first ? "length" : "stop" }),
269276
)
@@ -600,12 +607,14 @@ itOutputRetry.live("session.processor effect tests resample the exact request af
600607
expect(outputRetryInputs[1]).toBe(outputRetryInputs[0])
601608
expect(parts.filter((part) => part.type === "text").map((part) => part.text)).toStrictEqual(["complete"])
602609
const finishes = parts.filter((part) => part.type === "step-finish")
610+
const input = outputRetryUsage.truncated.input + outputRetryUsage.complete.input
611+
const output = outputRetryUsage.truncated.output + outputRetryUsage.complete.output
603612
expect(finishes).toHaveLength(1)
604613
expect(finishes[0]).toMatchObject({
605614
reason: "stop",
606-
tokens: { input: 10, output: 16 },
615+
tokens: { input, output },
607616
})
608-
expect(finishes[0]?.cost).toBeCloseTo(0.000026)
617+
expect(finishes[0]?.cost).toBeCloseTo((input + output) / 1_000_000)
609618
expect(handle.message.finish).toBe("stop")
610619
}),
611620
),

packages/opencode/test/session/retry.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ describe("session.retry.delay", () => {
116116
}),
117117
)
118118

119-
it.effect("policy caps output-length errors at two retries", () =>
119+
it.effect("policy caps output-length errors at three total calls", () =>
120120
Effect.gen(function* () {
121121
const error = new SessionV1.OutputLengthError({}).toObject()
122122
const attempts: number[] = []

0 commit comments

Comments
 (0)