Skip to content

feat(quota): gemini fetcher, grok token refresh, claude cred merge fix - #2665

Open
romeoju01 wants to merge 1 commit into
getpaseo:mainfrom
Aratives:quota-usage-fixes
Open

feat(quota): gemini fetcher, grok token refresh, claude cred merge fix#2665
romeoju01 wants to merge 1 commit into
getpaseo:mainfrom
Aratives:quota-usage-fixes

Conversation

@romeoju01

Copy link
Copy Markdown

Summary

Three independent quota-fetcher fixes, found while running the fetchers against real CLI credential files on Windows + WSL:

  • claude: \saveClaudeCredentials\ wrote back the zod-parsed document, which strips unknown keys — refreshing a token deleted \mcpOAuth, \expiresAt, \scopes, etc. from .credentials.json, degrading the user's Claude Code login. Now merges into the raw document.
  • grok: tokens in ~/.grok/auth.json\ expire every ~6h; an expired token silently made usage unavailable. Now: expired entries are skipped, and if every token is expired the fetcher runs the CLI's own OIDC flow (discovery →
    efresh_token\ grant) and persists the rotated tokens back to the same file (write-back is required — xAI rotates refresh tokens). Also adds \GROK_AUTH_FILE\ env override and a billing-period-end detail.
  • gemini: new fetcher for Code Assist subscriptions —
    etrieveUserQuota\ per-model buckets (remainingFraction/resetTime) as usage windows. Refreshes expired \oauth_creds.json\ via the gemini-cli's installed-app OAuth client, which is resolved at runtime from the local gemini-cli bundle (or \GEMINI_OAUTH_CLIENT_ID/\GEMINI_OAUTH_CLIENT_SECRET\ env) so no client credentials are committed. Project from \GOOGLE_CLOUD_PROJECT/\GOOGLE_CLOUD_QUOTA_PROJECT\ or constructor option; reports a descriptive error when unset.

Testing

  • \ sc -p packages/server/tsconfig.server.json\ — the four changed files compile clean (full-project build on this machine has unrelated pre-existing drift in \�cp-agent.ts/\session.ts\ from a stale local
    ode_modules, untouched by this PR).
  • Verified live against real credential files: claude (Max) usage windows, grok monthly credits after a real token expiry + refresh round-trip, gemini per-model buckets on a Code Assist project.
  • vitest not run locally (environment limitation); existing grok fixtures (tokens without \expires_at) are unaffected — they're treated as non-expiring, same as before.

- claude: merge refreshed oauth into the raw credentials document instead
  of the zod-parsed shape; the schema parse stripped sibling keys
  (mcpOAuth, expiresAt, scopes) on write-back, degrading the CLI login
- grok: skip expired auth.json tokens (expires_at) and refresh them via
  the CLI's OIDC flow (discovery + refresh_token grant), persisting the
  rotated tokens back to the same file; GROK_AUTH_FILE env override;
  billing-period-end detail
- gemini: new fetcher for Code Assist (retrieveUserQuota per-model
  buckets); refreshes expired oauth_creds.json via the CLI's installed-app
  OAuth client, resolved at runtime from the local gemini-cli bundle
  (GEMINI_OAUTH_CLIENT_ID/SECRET env overrides)
const target = entry.topKey ? raw[entry.topKey] : raw;
if (!target || typeof target !== "object" || Array.isArray(target)) return null;
const record = target as Record<string, unknown>;
record["key"] = tok.access_token;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Refreshed token stored in wrong field

When a legacy flat auth record contains an expired access_token, refresh writes the replacement to key while subsequent reads continue to prefer the stale access_token, causing later Grok quota requests to report usage as unavailable.

Suggested change
record["key"] = tok.access_token;
if (typeof record["access_token"] === "string") {
record["access_token"] = tok.access_token;
} else {
record["key"] = tok.access_token;
}

Comment on lines +67 to +68
if (cachedOAuthClient !== undefined) return cachedOAuthClient;
cachedOAuthClient = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Failed OAuth discovery stays cached

An unsuccessful OAuth-client lookup stores null in module state, so installing or repairing Gemini CLI later does not restore credential refresh until the long-running daemon restarts.

Suggested change
if (cachedOAuthClient !== undefined) return cachedOAuthClient;
cachedOAuthClient = null;
if (cachedOAuthClient) return cachedOAuthClient;
cachedOAuthClient = null;

@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds Gemini Code Assist quota retrieval and credential refresh, repairs Claude credential-file merging, and adds Grok token expiry handling, OIDC refresh, persistence, auth-file override support, and billing-period details.

Confidence Score: 4/5

The Grok legacy credential refresh defect should be fixed before merging because subsequent quota requests can revert to a stale token.

Grok refreshes legacy flat credentials into a different field than the parser subsequently prioritizes, while Gemini permanently caches transient OAuth-client discovery failures.

Files Needing Attention: packages/server/src/services/quota-fetcher/providers/grok.ts; packages/server/src/services/quota-fetcher/providers/gemini.ts

Important Files Changed

Filename Overview
packages/server/src/services/quota-fetcher/manifest.ts Registers the new Gemini quota provider through the existing provider manifest.
packages/server/src/services/quota-fetcher/providers/claude.ts Preserves unknown top-level Claude credential fields by merging refreshed OAuth data into the raw document.
packages/server/src/services/quota-fetcher/providers/gemini.ts Adds Gemini quota and token-refresh support, but a failed OAuth-client lookup is cached for the daemon lifetime.
packages/server/src/services/quota-fetcher/providers/grok.ts Adds expiry-aware Grok token selection and refresh, but legacy flat credentials revert to their stale access token after refresh.

Sequence Diagram

sequenceDiagram
  participant S as Quota service
  participant F as Credential file
  participant O as OAuth/OIDC server
  participant Q as Provider quota API
  S->>F: Read access token and expiry
  alt Token expired
    S->>O: Refresh token grant
    O-->>S: Rotated access/refresh tokens
    S->>F: Persist refreshed credentials
  end
  S->>Q: Fetch quota with access token
  Q-->>S: Usage buckets or balances
Loading

Reviews (1): Last reviewed commit: "feat(quota): gemini fetcher, grok token ..." | Re-trigger Greptile

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0151ac1ba8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

existing && typeof existing === "object" && !Array.isArray(existing)
? (existing as Record<string, unknown>)
: {};
record["claudeAiOauth"] = oauth;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve nested Claude OAuth metadata

When claudeAiOauth contains metadata such as expiresAt or scopes, oauth has already passed through ClaudeCredentialsSchema, which strips those unknown nested fields. Replacing the entire raw claudeAiOauth object here therefore still deletes that metadata during every token refresh, despite preserving root-level siblings; merge the refreshed token fields into the existing raw nested object instead.

Useful? React with 👍 / 👎.

Comment on lines +273 to +274
record["key"] = tok.access_token;
if (tok.refresh_token) record["refresh_token"] = tok.refresh_token;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Persist refreshed Grok tokens to the original field

For a supported auth entry that stores its token as access_token, entryFromValue gives that field precedence over key, but refresh writes only key. The current request succeeds with the returned token, while the next read selects the old access_token and treats it as fresh because expires_at was advanced, causing billing requests to fail until another refresh is attempted; update the field that supplied the token or update both representations.

Useful? React with 👍 / 👎.

Comment on lines +117 to +122
const nvmRoot = join(homedir(), ".nvm", "versions", "node");
try {
if (existsSync(nvmRoot)) {
for (const ver of readdirSync(nvmRoot)) {
dirs.push(join(nvmRoot, ver, "lib", "node_modules", "@google", "gemini-cli", "bundle"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Search the configured npx installation for Gemini OAuth

The repository's configured Gemini command is npx -y @google/gemini-cli@0.52.0 (packages/app/src/data/acp-provider-catalog.ts:188), but this lookup searches only an APPDATA global installation and NVM global installations. An npx-managed package resides in npm's execution cache, so after its access token expires, the default Paseo installation cannot discover the CLI OAuth client and returns unavailable unless users manually supply undocumented override variables.

Useful? React with 👍 / 👎.

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.

1 participant