From b5e2a290bc506129d87980b3d40540140b755fe4 Mon Sep 17 00:00:00 2001 From: abdfattahz <203197396+abdfattahz@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:01:40 +0800 Subject: [PATCH] fix(server): OpenCode ChatGPT usage limits keep their reset time ChatGPT-backed OpenAI models answer usage limits with a 429 whose body carries error.resets_at. Read it from the failure's response body so the usage-limit failure gets a resetAt, enabling resume and snooze at reset. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/OpenCode2AdapterV2.test.ts | 18 +++++++++++++++++- .../provider-opencode/src/server/v2/adapter.ts | 15 ++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts index 37350b4925cf9..459087240e5dc 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts @@ -554,6 +554,21 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { status: 429, }, }, + { + name: "a ChatGPT provider.quota usage window (429)", + failureClass: "usage_limit", + error: { + type: "provider.quota", + message: "The usage limit has been reached", + status: 429, + response: { + body: JSON.stringify({ + error: { type: "usage_limit_reached", resets_at: 1793114741, resets_in_seconds: 222 }, + }), + }, + }, + resetAt: "2026-10-27T15:25:41.000Z", + }, { name: "a provider.rate-limit failure (429)", failureClass: "usage_limit", @@ -563,7 +578,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { status: 429, }, }, - ] as const)("classifies $name as $failureClass", ({ error, failureClass }) => + ] as const)("classifies $name as $failureClass", ({ error, failureClass, ...row }) => Effect.gen(function* () { const { runtime, thread } = yield* resumed([ out("session.prompt", { sessionID: SESSION, text: "" }), @@ -579,6 +594,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { code: error.type, message: error.message, retryable: null, + ...("resetAt" in row ? { resetAt: row.resetAt } : {}), }); }).pipe(Effect.scoped), ); diff --git a/packages/provider-opencode/src/server/v2/adapter.ts b/packages/provider-opencode/src/server/v2/adapter.ts index 9c4ec57c34776..cf2076c4e7e68 100644 --- a/packages/provider-opencode/src/server/v2/adapter.ts +++ b/packages/provider-opencode/src/server/v2/adapter.ts @@ -45,6 +45,7 @@ import * as Effect from "effect/Effect"; import * as Hex from "effect/encoding/Hex"; import * as Exit from "effect/Exit"; import * as FileSystem from "effect/FileSystem"; +import * as Option from "effect/Option"; import * as Queue from "effect/Queue"; import * as Schedule from "effect/Schedule"; import * as Scope from "effect/Scope"; @@ -518,6 +519,17 @@ const wakeTurnId = (sessionId: string, attemptId: string) => `${sessionId}:wake: const isWakeTurn = (turn: OrchestrationV2ProviderTurn) => turn.nativeTurnRef?.nativeId?.includes(":wake:") === true; +// ChatGPT-backed OpenAI models put the usage-limit reset in the 429 body as Unix seconds. +const decodeUsageLimitBody = Schema.decodeUnknownOption( + Schema.fromJsonString(Schema.Struct({ error: Schema.Struct({ resets_at: Schema.Finite }) })), +); +const usageLimitResetAt = (body: string | undefined) => + decodeUsageLimitBody(body).pipe( + Option.flatMap(({ error }) => DateTime.make(error.resets_at * 1000)), + Option.map(DateTime.formatIso), + Option.getOrNull, + ); + const INTERRUPT_TIMEOUT = "10 seconds"; const INSTRUCTIONS_KEY = "t3-code"; const RECONNECT_ATTEMPTS = 5; @@ -2125,7 +2137,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr case "session.execution.interrupted": return yield* endExecution(state, turn, { status: "interrupted" }); case "session.execution.failed": { - const { type, message, status } = event.data.error; + 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 usageLimit = @@ -2136,6 +2148,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr message, code: type, class: usageLimit ? "usage_limit" : "provider_error", + resetAt: usageLimitResetAt(response?.body), }), }); }