Skip to content

feat(auth): add preferred credential quota fallback - #3833

Draft
najy97 wants to merge 2 commits into
Yeachan-Heo:devfrom
najy97:feat/preferred-credential-quota-fallback
Draft

feat(auth): add preferred credential quota fallback#3833
najy97 wants to merge 2 commits into
Yeachan-Heo:devfrom
najy97:feat/preferred-credential-quota-fallback

Conversation

@najy97

@najy97 najy97 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add --prefer-credential as a soft stored OAuth credential preference
  • preserve --credential as a hard pin while rotating preferred credentials on content-free quota/rate-limit failures
  • infer the provider for unqualified selectors such as id:15, so a default model from another provider cannot capture the preference
  • keep fallback credentials sticky for the session and reject selector/API-key conflicts at startup
  • redact credential identities from usage logs in favor of stored row IDs
  • support any provider backed by a multi-account OAuth pool, not only Anthropic

Behavior

  • preferred and usable: placed before the existing balanced ranking
  • preferred and blocked: skipped; remaining candidates retain balanced ordering
  • content-free quota/rate limit: mark blocked, switch rows, retry immediately
  • partial output/tool execution: no automatic replay
  • forbidden: no quota mutation
  • all rows blocked: no retry loop
  • ambiguous cross-provider selector: fail and require provider/<selector>

API-key credentials are intentionally excluded from soft preference; --api-key is mutually exclusive with this option.

Verification

  • 70 focused tests pass across AuthStorage, CLI parsing, credential rotation E2E, and SDK model selection
  • regression test covers an unqualified preferred Anthropic credential when the configured default model is Amazon Bedrock
  • bun --cwd=packages/ai run check:types
  • bun --cwd=packages/coding-agent run check:types
  • Biome check on all affected TypeScript files
  • live local smoke: gjc -p ... --prefer-credential id:15 returns successfully

@najy97
najy97 marked this pull request as draft August 5, 2026 04:51

@Yeachan-Heo Yeachan-Heo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #3833 — feat(auth): add preferred credential quota fallback

Reviewed exact head 247e181bc19329c4980491e9017f56f2390c1a08 (commits da781bf3d, 247e181bc) against base 732856b3c. Full diff inspected; PR tests run locally (6 auth-storage + 49 cli-args + 13 sdk-model-selection = 68 pass, 0 fail); tsc --noEmit clean for packages/ai and packages/coding-agent; biome clean on all 9 changed TS files; docs-index.generated.ts verified in sync against all 120 docs entries. The --prefer-credential plumbing (CLI parse + mutual exclusion with --credential/--api-key at parse, main, and AuthStorage layers), provider inference, and quota-fallback reordering are consistent and fail closed. Privacy is improved: usage logs no longer carry emails/account ids.

Blocker — #getUsageReportCredentialRowId never resolves: "Usage fetch resolved" logs lose credential attribution

packages/ai/src/auth-storage.ts:2733 compares provider-prefixed report identifiers against unprefixed credential identity keys:

  • #getUsageReportIdentifiers() returns anthropic:email:primary@example.test (both branches prefix ${report.provider}:).
  • resolveCredentialIdentityKey() returns email:primary@example.test (unprefixed, for anthropic/openai-codex; account:... for others).

identifiers.has(identityKey.toLowerCase()) therefore never matches, so #getUsageReportCredentialRowId always returns undefined. The rewritten Usage fetch resolved debug log ({provider, limits, credentialId}) never emits credentialId — the row-ID attribution that replaces the removed accountId/email fields silently disappears. This regresses observability versus the old code, which logged the account label.

Confirmed empirically: with the PR's own test setup, the queued log lines carry credentialId:1/2 (via #findStoredCredentialIdForUsageCredential, token matching) but the resolved log lines are {"provider":"anthropic","limits":0} with no credentialId at all.

The new test "usage logs identify credentials only by row ID" passes for the wrong reason: its serialized assertion is satisfied by the queued-log lines and never exercises the resolved path, masking the defect.

Fix direction: align the formats (compare identityKey against unprefixed identifiers, or prefix the credential identity key) and tighten the test to assert credentialId specifically in the Usage fetch resolved log line.

Non-blocking notes

  • packages/coding-agent/src/sdk/session.ts: the deferred per-candidate preferredCredentialSelector path (availability loop + late install at createAgentSession end) is unreachable — preferredCredentialProvider is always resolved up-front, so runtimePreferredCredentialSelectorInstalled is always true when the option is present. Dead code worth simplifying.
  • The "fallback to first available model" loop (session.ts ~2121) does not filter candidates by preferredCredentialProvider, so in edge cases it can resolve a different-provider model and then hit the hard throw at line 2168. Fail closed, but a provider-scoped candidate filter would give a cleaner error path.
  • removeRuntimePreferredCredentialSelector is currently unused (consistent with removeRuntimeCredentialSelector, also only test-used).
  • Mid-session deletion of the preferred row makes subsequent getApiKey calls throw (fail closed) rather than rotate; acceptable but worth documenting.
  • On --resume with a healthy sticky session credential, session stickiness overrides the explicit preference (preferred row only wins when the sticky row is blocked). Matches the documented "fallback row remains sticky for the session" design, but users should be aware the preference is soft on resume.
  • PR head is 22 commits behind dev; drift touches session.ts/main.ts (memory-startup) and the docs index only — no conflicts (mergeable: true), no interaction found with this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants