feat(auth): add preferred credential quota fallback - #3833
Conversation
Yeachan-Heo
left a comment
There was a problem hiding this comment.
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()returnsanthropic:email:primary@example.test(both branches prefix${report.provider}:).resolveCredentialIdentityKey()returnsemail: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-candidatepreferredCredentialSelectorpath (availability loop + late install atcreateAgentSessionend) is unreachable —preferredCredentialProvideris always resolved up-front, soruntimePreferredCredentialSelectorInstalledis 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 bypreferredCredentialProvider, 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. removeRuntimePreferredCredentialSelectoris currently unused (consistent withremoveRuntimeCredentialSelector, also only test-used).- Mid-session deletion of the preferred row makes subsequent
getApiKeycalls throw (fail closed) rather than rotate; acceptable but worth documenting. - On
--resumewith 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 touchessession.ts/main.ts(memory-startup) and the docs index only — no conflicts (mergeable: true), no interaction found with this change.
Summary
--prefer-credentialas a soft stored OAuth credential preference--credentialas a hard pin while rotating preferred credentials on content-free quota/rate-limit failuresid:15, so a default model from another provider cannot capture the preferenceBehavior
provider/<selector>API-key credentials are intentionally excluded from soft preference;
--api-keyis mutually exclusive with this option.Verification
bun --cwd=packages/ai run check:typesbun --cwd=packages/coding-agent run check:typesgjc -p ... --prefer-credential id:15returns successfully