diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts index 89458b1deeeda..19b055f38aab3 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts @@ -648,6 +648,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", @@ -673,7 +688,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: "" }), @@ -705,6 +720,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { code: error.type, message: error.message, retryable: null, + ...("resetAt" in row ? { resetAt: row.resetAt } : {}), }); }), ); diff --git a/packages/provider-opencode/src/server/v2/adapter.ts b/packages/provider-opencode/src/server/v2/adapter.ts index 3a83c81557d5a..eec28048e1b3d 100644 --- a/packages/provider-opencode/src/server/v2/adapter.ts +++ b/packages/provider-opencode/src/server/v2/adapter.ts @@ -609,6 +609,17 @@ const isUsageLimit = (error: { error.type === "provider.rate-limit" || claudeUsageLimit(error.message) !== 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_000; const INSTRUCTIONS_KEY = "t3-code"; const RECONNECT_ATTEMPTS = 5; @@ -2889,7 +2900,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr message: readableProviderErrorMessage(message, status, response?.body), code: type, class: isUsageLimit(event.data.error) ? "usage_limit" : "provider_error", - resetAt: claudeUsageLimit(message)?.resetAt ?? null, + resetAt: claudeUsageLimit(message)?.resetAt ?? usageLimitResetAt(response?.body), }), }); }