diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts index c334af4fad113..ba73db7f1c43f 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts @@ -561,6 +561,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", @@ -586,7 +601,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { message: "Request failed. Resets at 2026-11-02T20:00:00Z.", }, }, - ] 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: "" }), @@ -602,6 +617,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 2e0f76c34ea74..9a43687ea4f08 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"; @@ -531,6 +532,17 @@ const claudeUsageLimit = (message: string | undefined) => ? { resetAt: /\bResets at (\d{4}-\d\d-\d\dT[\d:.]+Z)\.?\s*$/.exec(message)?.[1] ?? null } : undefined; +// 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; @@ -2168,7 +2180,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr message: readableProviderErrorMessage(message, status, response?.body), code: type, class: usageLimit ? "usage_limit" : "provider_error", - resetAt: claudeLimit?.resetAt ?? null, + resetAt: claudeLimit?.resetAt ?? usageLimitResetAt(response?.body), }), }); }