Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions packages/opencode/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface Interface {
readonly set: (key: string, info: Info) => Effect.Effect<void, AuthError>
readonly remove: (key: string) => Effect.Effect<void, AuthError>
readonly removeKey: (providerID: string, index: number) => Effect.Effect<void, AuthError>
readonly advanceKey: (providerID: string) => Effect.Effect<void, AuthError>
}

export class Service extends Context.Service<Service, Interface>()("@opencode/Auth") {}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 })
}),
)

Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/session/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/session/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading