diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts index acf2f628f5126..73dbb42ca8752 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts @@ -677,11 +677,27 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { const { runtime, thread } = yield* resumed([ out("session.prompt", { sessionID: SESSION, text: "" }), promptAccepted, + event("session.retry.scheduled", { + sessionID: SESSION, + assistantMessageID: "msg_0eb735d5b001oAFVeY5jz3WD4Z", + attempt: 2, + at: 2_001, + error, + }), event("session.execution.failed", { sessionID: SESSION, error }), ]); - const terminal = yield* terminalOf(runtime).pipe(Effect.forkScoped); + const collected = yield* runtime.events.pipe( + Stream.takeUntil((event) => event.type === "turn.terminal"), + Stream.runCollect, + Effect.forkScoped, + ); yield* runtime.startTurn(turnInput(thread)); - const ended = yield* Fiber.join(terminal); + const seen = yield* Fiber.join(collected); + assert.deepInclude(errorRows(seen)[0], { + status: "running", + failure: { class: failureClass, code: error.type, message: error.message, retryable: true }, + }); + const ended = seen.find((event) => event.type === "turn.terminal"); assert.equal(ended?.status, "failed"); assert.deepEqual(ended?.failure, { class: failureClass, @@ -5479,7 +5495,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { error: rateLimited, }); const retrying = { - class: "provider_error", + class: "usage_limit", message: rateLimited.message, code: rateLimited.type, retryable: true, @@ -5661,7 +5677,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { ], gate, ); - const failed = { ...retrying, class: "usage_limit" as const, retryable: null }; + const failed = { ...retrying, retryable: null }; const lastRetry = { attempt: 2, maxAttempts: null, retryDelayMs: 4_000 }; const rows = errorRows(seen); assert.deepEqual( diff --git a/packages/provider-opencode/src/server/v2/adapter.ts b/packages/provider-opencode/src/server/v2/adapter.ts index 95cee021b2774..6604f84eb289e 100644 --- a/packages/provider-opencode/src/server/v2/adapter.ts +++ b/packages/provider-opencode/src/server/v2/adapter.ts @@ -590,6 +590,17 @@ const claudeUsageLimit = (message: string | undefined) => ? { resetAt: /\bResets at (\d{4}-\d\d-\d\dT[\d:.]+Z)\.?\s*$/.exec(message)?.[1] ?? null } : undefined; +// OpenCode types an empty balance as provider.quota too, but it answers +// that with HTTP 402 and it never resets. Usage windows and budgets are 429. +const isUsageLimit = (error: { + readonly type: string; + readonly status?: number | undefined; + readonly message: string; +}) => + (error.type === "provider.quota" && error.status !== 402) || + error.type === "provider.rate-limit" || + claudeUsageLimit(error.message) !== undefined; + const INTERRUPT_TIMEOUT = 10_000; const INSTRUCTIONS_KEY = "t3-code"; const RECONNECT_ATTEMPTS = 5; @@ -1465,7 +1476,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr failure: makeProviderFailure({ message: readableProviderErrorMessage(error.message, error.status, error.response?.body), code: error.type, - class: "provider_error", + class: isUsageLimit(error) ? "usage_limit" : "provider_error", retryable: true, }), startedAt: turn.retry?.startedAt ?? (yield* DateTime.now), @@ -2839,20 +2850,13 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr return yield* endExecution(state, turn, { status: "interrupted" }); case "session.execution.failed": { const { type, message, status, response } = event.data.error; - // OpenCode types an empty balance as provider.quota too, but it answers - // that with HTTP 402 and it never resets. Usage windows and budgets are 429. - const claudeLimit = claudeUsageLimit(message); - const usageLimit = - (type === "provider.quota" && status !== 402) || - type === "provider.rate-limit" || - claudeLimit !== undefined; return yield* endExecution(state, turn, { status: "failed", failure: makeProviderFailure({ message: readableProviderErrorMessage(message, status, response?.body), code: type, - class: usageLimit ? "usage_limit" : "provider_error", - resetAt: claudeLimit?.resetAt ?? null, + class: isUsageLimit(event.data.error) ? "usage_limit" : "provider_error", + resetAt: claudeUsageLimit(message)?.resetAt ?? null, }), }); }