can1357/oh-my-pi · error · AIError.MissingApiKeyError

No OAuth credential available for provider: ${provider}

Error message

No OAuth credential available for provider: ${provider}

What it means

withOAuthAccess runs an auth-protected operation with automatic credential rotation. Before the first attempt it fetches the current OAuth access (or uses opts.seed); if none exists it throws MissingApiKeyError because there is no credential to authenticate with at all. This is thrown up front, not after a failed request.

Source

Thrown at packages/ai/src/auth-retry.ts:337

 * sibling rotation stops when it yields a credential identity
 * (`credentialId ?? accessToken`) or bearer already attempted in this turn.
 * All OAuth attempts share the {@link AUTH_RETRY_MAX_ATTEMPTS} ceiling.
 * Non-auth errors propagate immediately. Use this instead of hand-rolled
 * `getOAuthAccess` + fetch flows so 401s and usage-limits rotate credentials
 * instead of failing the call.
 */
export async function withOAuthAccess<T>(
	storage: OAuthAccessSource,
	provider: string,
	attempt: (access: OAuthAccess) => Promise<T>,
	opts?: WithOAuthAccessOptions,
): Promise<T> {
	const isAuthError = opts?.isAuthError ?? isAuthRetryableError;
	const { sessionId, signal } = opts ?? {};

	let lastAccess = opts?.seed ?? (await storage.getOAuthAccess(provider, sessionId, { signal }));
	if (!lastAccess) {
		throw new AIError.MissingApiKeyError(
			provider,
			opts?.missingAccessMessage ?? `No OAuth credential available for provider: ${provider}`,
		);
	}

	const attemptedBearers = new Set([lastAccess.accessToken]);
	const attemptedCredentialIdentities = new Set([oauthCredentialIdentity(lastAccess)]);
	let attemptCount = 1;
	let legacyAuthSwitchUsed = false;
	let refreshedCurrent = false;
	let tokenRefreshReplayUsed = false;
	let attemptResult = await runOAuthAttempt(lastAccess, attempt, isAuthError);
	if (attemptResult.ok) return attemptResult.result;

	let lastError = attemptResult.error;
	while (true) {
		let next: OAuthAccess | undefined;
		if (signal?.aborted || attemptCount >= AUTH_RETRY_MAX_ATTEMPTS) break;

View on GitHub (pinned to 9690622007)

Solutions

  1. Run the OAuth login flow for the provider before calling (storage login / loginOAuth) so a credential exists
  2. Check the provider string matches the provider you authenticated against (exact spelling)
  3. If credentials are session-scoped, ensure the credential exists for that sessionId or omit sessionId to use the default
  4. Register a clear logout/re-login handler so expired/removed credentials prompt re-auth

Example fix

// before
const result = await withOAuthAccess(storage, "codex", callApi);
// after
if (!(await storage.getOAuthAccess("codex", sessionId))) {
	await storage.loginOAuth("codex", { onAuth: saveAuth }); // obtain credential first
}
const result = await withOAuthAccess(storage, "codex", callApi);
Defensive patterns

Strategy: validation

Validate before calling

const access = await storage.getOAuthAccess(provider, sessionId);
if (!access) throw new Error(`Run OAuth login for ${provider} first`);

Try / catch

try {
	return await withOAuthAccess(storage, provider, attempt);
} catch (error) {
	if (error instanceof AIError.MissingApiKeyError) {
		await triggerLoginFlow(provider); // prompt re-auth
	}
	throw error;
}

Prevention

When it happens

Trigger: Calling withOAuthAccess(storage, provider, ...) (directly or via helpers like searchCodex/searchGemini) when storage.getOAuthAccess(provider, sessionId) returns undefined and no opts.seed is provided — i.e. no OAuth credential is stored for that provider/session.

Common situations: User never ran the OAuth login flow for the provider; credential was logged out or removed from storage; wrong provider string passed (typo or runtime-registered provider without stored creds); using a sessionId that has no session-scoped credential.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/d0226672917dcba0. Report an issue: GitHub.