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
85 changes: 85 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,91 @@ export function buildAccountLabels(creds) { return creds.map((_, i) => \`Account
}
}
})

it("auth fetch returns quota errors without writing over the terminal UI", async () => {
const originalNow = Date.now
const originalSetInterval = globalThis.setInterval
const originalHome = process.env.HOME
const originalFetch = globalThis.fetch
const originalWarn = console.warn
const tempHome = await mkdtemp(join(tmpdir(), "opencode-claude-auth-home-"))
process.env.HOME = tempHome
Date.now = () => 1_700_000_000_000
globalThis.setInterval = (() => ({
unref() {},
})) as unknown as typeof setInterval

const errorBody = JSON.stringify({
type: "error",
error: {
type: "rate_limit_error",
message:
"This request would exceed your account's rate limit. Please try again later.",
},
})
let fetchCount = 0
const warnings: unknown[][] = []

try {
const { helpersModule } = await loadHelpersWithCountingKeychain(
Date.now() + 10 * 60_000,
)
globalThis.fetch = (async () => {
fetchCount += 1
return new Response(errorBody, {
status: 429,
headers: {
"content-type": "application/json",
"retry-after": "11218",
},
})
}) as typeof fetch
console.warn = (...args: unknown[]) => {
warnings.push(args)
}

const plugin = await helpersModule.default({} as never)
const typedPlugin = plugin as { auth?: { loader?: TestAuthLoader } }
assert.equal(typeof typedPlugin.auth?.loader, "function")
const authConfig = await typedPlugin.auth!.loader!(
async () => ({
type: "oauth",
refresh: "refresh",
access: "access",
expires: Date.now() + 60_000,
}),
{ models: {} },
)

const response = await authConfig.fetch(
"https://api.anthropic.com/v1/messages",
{
method: "POST",
body: JSON.stringify({
model: "claude-opus-4-8",
messages: [],
}),
},
)

assert.equal(response.status, 429)
assert.equal(response.headers.get("retry-after"), "11218")
assert.equal(await response.text(), errorBody)
await new Promise<void>((resolve) => setImmediate(resolve))
assert.equal(fetchCount, 1)
assert.deepEqual(warnings, [])
} finally {
Date.now = originalNow
globalThis.setInterval = originalSetInterval
globalThis.fetch = originalFetch
console.warn = originalWarn
if (typeof originalHome === "string") {
process.env.HOME = originalHome
} else {
delete process.env.HOME
}
}
})
})

describe("auth hook — account resolution", () => {
Expand Down
5 changes: 1 addition & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ const plugin: Plugin = async () => {
})
}

// Log non-200 responses at warn level so they're visible in OpenCode
// Record non-200 responses without writing over OpenCode's terminal UI.
if (!response.ok) {
const status = response.status
const cloned = response.clone()
Expand All @@ -464,9 +464,6 @@ const plugin: Plugin = async () => {
parsed.error?.message ?? parsed.error?.type ?? errorBody
} catch {}
log("fetch_error_response", { status, modelId, message })
console.warn(
`opencode-claude-auth: API ${status} for ${modelId}: ${message}`,
)
})
.catch(() => {})
}
Expand Down
Loading