From 8917d7b0d90a3274d7a68d402e04653b3e43015f Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 3 Aug 2026 07:32:19 -0700 Subject: [PATCH 1/2] fix(failover): zero delay when rotating keys within same provider --- packages/opencode/src/session/retry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index af44daefe6f9..ae0bab134e68 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -193,7 +193,7 @@ export function policy(opts: { Effect.flatMap((switched) => { if (!retry && !switched) return Cause.done(meta.attempt) return Effect.gen(function* () { - const wait = switched ? 0 : delay(meta.attempt, SessionV1.APIError.isInstance(error) ? error : undefined) + const wait = switched || eligible ? 0 : delay(meta.attempt, SessionV1.APIError.isInstance(error) ? error : undefined) const now = yield* Clock.currentTimeMillis if (!retry) { yield* opts.set({ From 91251b916cdacb6a601c4db9ad6e5aa7e8424ed5 Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 3 Aug 2026 07:34:07 -0700 Subject: [PATCH 2/2] fix(failover): stick with one key until it fails, then advance --- packages/opencode/src/auth/index.ts | 17 +++++++++++++++-- packages/opencode/src/session/processor.ts | 3 ++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 66c22c9f02a6..af679387e17c 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -48,6 +48,7 @@ export interface Interface { readonly set: (key: string, info: Info) => Effect.Effect readonly remove: (key: string) => Effect.Effect readonly removeKey: (providerID: string, index: number) => Effect.Effect + readonly advanceKey: (providerID: string) => Effect.Effect } export class Service extends Context.Service()("@opencode/Auth") {} @@ -75,12 +76,24 @@ const layer = Layer.effect( const keys = parseKeys(info) if (keys.length === 0) return info const index = rotations.get(providerID) ?? 0 - rotations.set(providerID, (index + 1) % keys.length) const selectedKey = keys[index % keys.length] console.log(`[key-rotation] ${providerID} using key ${index % keys.length + 1}/${keys.length}: ${selectedKey.slice(0, 6)}...${selectedKey.slice(-4)}`) return new Api({ ...info, key: selectedKey }) }) + const advanceKey = Effect.fn("Auth.advanceKey")(function* (providerID: string) { + if (!multiKeyProviders.has(providerID)) return + const info = (yield* all())[providerID] + if (!info || info.type !== "api") return + const keys = parseKeys(info) + if (keys.length === 0) return + const current = rotations.get(providerID) ?? 0 + const next = (current + 1) % keys.length + rotations.set(providerID, next) + const selectedKey = keys[next] + console.log(`[key-rotation] ${providerID} advancing to key ${next + 1}/${keys.length}: ${selectedKey.slice(0, 6)}...${selectedKey.slice(-4)}`) + }) + const set = Effect.fn("Auth.set")(function* (key: string, info: Info) { const norm = key.replace(/\/+$/, "") const data = yield* all() @@ -133,7 +146,7 @@ const layer = Layer.effect( yield* fsys.writeJson(file, data, 0o600).pipe(Effect.mapError(fail("Failed to write auth data"))) }) - return Service.of({ get, all, set, remove, removeKey }) + return Service.of({ get, all, set, remove, removeKey, advanceKey }) }), ) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 6ad19dfbaae1..e74f4950396a 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -136,7 +136,8 @@ const layer = Layer.effect( const keys = currentAuth.metadata?.keys ? JSON.parse(currentAuth.metadata.keys) : [currentAuth.key] if (Array.isArray(keys) && retryKeyCount < keys.length - 1) { retryKeyCount++ - console.log(`[failover-v1] ${ctx.model.providerID} key ${retryKeyCount}/${keys.length} failed, trying next key before switching provider`) + yield* auth.advanceKey(ctx.model.providerID).pipe(Effect.orDie) + console.log(`[failover-v1] ${ctx.model.providerID} key ${retryKeyCount}/${keys.length} failed, advancing to next key`) return false } console.log(`[failover-v1] ${ctx.model.providerID} all ${keys.length} keys exhausted, switching provider`)