Skip to content

fix(providers): propagate quota project to Vertex via X-Goog-User-Project#325

Merged
SantiagoDePolonia merged 1 commit into
mainfrom
fix/vertex-quota-project
May 12, 2026
Merged

fix(providers): propagate quota project to Vertex via X-Goog-User-Project#325
SantiagoDePolonia merged 1 commit into
mainfrom
fix/vertex-quota-project

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented May 12, 2026

Summary

  • Switch internal/providers/googleauth from google.DefaultTokenSource / google.JWTConfigFromJSON to google.FindDefaultCredentials / google.CredentialsFromJSONWithType, and expose a new Credentials wrapper that carries the resolved quota project alongside the token source.
  • Parse quota_project_id from the raw ADC JSON ourselves — *google.Credentials.ProjectID only surfaces SA project_id, the authorized-user ADC quota override never propagates through the public struct.
  • Add a quotaProject parameter to googleauth.HTTPClient. When non-empty, a small RoundTripper wrapper sets X-Goog-User-Project on every outbound request (above the existing OAuth2 token transport).
  • Vertex and Gemini-on-Vertex providers prefer the credential-supplied quota project, then fall back to VERTEX_PROJECT. Single-project deployments work out-of-the-box without gcloud auth application-default set-quota-project.
  • TokenSource() is kept as a thin wrapper around FindCredentials for callers that only need the token source.

Why

Surfaced during live testing of #324. With ADC user credentials (the common gcloud auth application-default login flow) GoModel was sending requests to aiplatform.googleapis.com without an X-Goog-User-Project header. Google's Vertex API responds:

authentication_error: Your application is authenticating by using local
Application Default Credentials. The aiplatform.googleapis.com API requires
a quota project, which is not set by default.

This blocked every ADC-based Vertex deployment, regardless of whether the operator had run gcloud auth application-default set-quota-project. Even when set, the quota project lived in application_default_credentials.json and was never read.

Test plan

  • make test-race, make lint, go mod tidy, mint validate — all green via pre-commit hooks.
  • New googleauth tests:
    • TestFindCredentialsReadsADCQuotaProject — ADC user JSON with quota_project_id round-trips through FindCredentials.
    • TestFindCredentialsReadsServiceAccountProject — SA JSON with project_id round-trips through FindCredentials.
    • TestHTTPClientSetsQuotaProjectHeader — upstream receives X-Goog-User-Project: <project> when set.
    • TestHTTPClientOmitsQuotaProjectHeaderWhenEmpty — header is omitted entirely when not set.
  • New vertex integration test TestNewSetsQuotaProjectHeaderOnVertexRequests with a live httptest upstream:
    • ADC quota_project_id=billing-project + VERTEX_PROJECT=prod-ai → upstream sees X-Goog-User-Project: billing-project (credential wins).
    • ADC without quota_project_id + VERTEX_PROJECT=prod-ai → upstream sees X-Goog-User-Project: prod-ai (fallback).
  • Existing googleauth, vertex, and gemini tests updated for the new HTTPClient signature; all pass under -race.

Compat

No behavior change for service-account auth, AI Studio Gemini, or any non-Google provider. ADC users who already had a quota project set in their application_default_credentials.json will start seeing it propagated automatically; users without one fall back to VERTEX_PROJECT, which has always been required for the Vertex provider anyway.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added automatic quota project identification and header injection for Google API requests, enabling improved billing and resource attribution tracking.
  • Improvements

    • Enhanced credential resolution for Google Cloud authentication providers, including quota project extraction from service accounts and application default credentials.
  • Tests

    • Expanded test coverage for quota project credential loading and header injection validation.

Review Change Stack

…ject

Vertex and Gemini-on-Vertex callers wired googleauth to google.DefaultTokenSource,
which returns only a token — the `quota_project_id` in
`application_default_credentials.json` was never read and the
`X-Goog-User-Project` header was never sent. Result: any ADC-based Vertex
deployment hit `authentication_error: ... API requires a quota project` against
aiplatform.googleapis.com, exactly as observed when smoke-testing the recent
provider-naming PR.

Switch googleauth to google.FindDefaultCredentials /
google.CredentialsFromJSONWithType and expose a Credentials wrapper carrying
the resolved quota project. For service-account JSON, that's `project_id`; for
authorized-user ADC the public *google.Credentials leaves ProjectID empty, so
parse `quota_project_id` from the raw JSON ourselves. HTTPClient gains a
quotaProject parameter that injects X-Goog-User-Project via a small
RoundTripper wrapper when non-empty. Vertex and Gemini-on-Vertex providers
fall back to VERTEX_PROJECT when the credential material doesn't supply one,
so the common single-project case works out-of-the-box without `gcloud auth
application-default set-quota-project`.

TokenSource() is kept as a thin wrapper around FindCredentials for callers
that only need the token source.

Tests:
- googleauth: FindCredentials reads ADC quota_project_id and SA project_id;
  HTTPClient sets the header when provided and omits it when empty.
- vertex: live httptest upstream verifies ADC `quota_project_id` overrides
  VERTEX_PROJECT and that VERTEX_PROJECT is used when quota project is absent.
- Existing googleauth, vertex, and gemini tests updated for the new HTTPClient
  signature; all pass under -race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e9e52313-390e-4e5a-8cb3-2e8ef5c3affc

📥 Commits

Reviewing files that changed from the base of the PR and between 6854023 and 8ce083d.

📒 Files selected for processing (6)
  • internal/providers/gemini/gemini.go
  • internal/providers/gemini/gemini_test.go
  • internal/providers/googleauth/googleauth.go
  • internal/providers/googleauth/googleauth_test.go
  • internal/providers/vertex/vertex.go
  • internal/providers/vertex/vertex_test.go

📝 Walkthrough

Walkthrough

The PR implements quota-project support for Google Cloud authentication. A new Credentials struct pairs token sources with quota project IDs extracted from service-account or ADC credentials. FindCredentials replaces prior token-only resolution, and HTTPClient now accepts a quota-project argument to conditionally inject the X-Goog-User-Project header. Both Gemini and Vertex providers are updated to use the new credential flow.

Changes

Quota-Project Support across Providers

Layer / File(s) Summary
Core credential and HTTP client refactoring
internal/providers/googleauth/googleauth.go
Introduces Credentials struct pairing token source and quota project ID, adds FindCredentials to resolve both from service account or ADC, updates HTTPClient to accept and conditionally inject the X-Goog-User-Project header via a new transport wrapper, and maintains TokenSource as a thin wrapper.
Credential resolution and header behavior tests
internal/providers/googleauth/googleauth_test.go
Refactors ADC credential helper to support optional quota project, adds tests verifying FindCredentials reads quota_project_id from ADC and project_id from service accounts, and tests HTTPClient header injection and omission.
Gemini provider quota-project integration
internal/providers/gemini/gemini.go, internal/providers/gemini/gemini_test.go
Updates authHTTPClient to use FindCredentials instead of TokenSource, derives quota project from credentials with fallback to VertexProject config, and passes both to HTTPClient; test helper updated to include quota-project argument.
Vertex provider quota-project integration
internal/providers/vertex/vertex.go, internal/providers/vertex/vertex_test.go
Updates authHTTPClient to use FindCredentials with credential-error handling, derives quota project from credentials with fallback to VertexProject config, and passes both to HTTPClient; adds test helper for ADC credentials with quota project and new test verifying header behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • ENTERPILOT/GoModel#302: Introduced the Google auth plumbing and provider integration that this PR extends with quota-project support.

Poem

🐰 A quota project in every request,
Google Cloud routing at its best!
From credentials parsed to headers sent,
Your infrastructure's intent is meant.
No more guessing where calls should go—
The X-Goog-User-Project will show!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: propagating quota project to Vertex via X-Goog-User-Project header. It directly reflects the primary objective of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/vertex-quota-project

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 66.66667% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/providers/googleauth/googleauth.go 71.42% 5 Missing and 5 partials ⚠️
internal/providers/gemini/gemini.go 0.00% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@SantiagoDePolonia SantiagoDePolonia merged commit d3d1c10 into main May 12, 2026
15 of 18 checks passed
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 12, 2026

Greptile Summary

This PR fixes a live ADC regression on Vertex AI by propagating the quota project through a new X-Goog-User-Project header. It introduces FindCredentials (replacing the direct TokenSource call) to carry both the token source and the resolved quota project, and adds a quotaProjectTransport RoundTripper that injects the header as the outermost layer above oauth2.Transport, ensuring the header survives the OAuth2 transport's own request clone.

  • googleauth.go gains FindCredentials, resolveQuotaProject (reads creds.ProjectID for SA, then falls back to JSON-parsing quota_project_id for ADC user creds), and quotaProjectTransport. The backward-compatible TokenSource wrapper is kept for callers that don't need the project.
  • vertex.go and gemini.go switch to FindCredentials and apply the same fallback: credential-supplied quota project wins, then VertexProject. Four new googleauth unit tests and one vertex integration test with live httptest servers cover the new paths.

Confidence Score: 4/5

Safe to merge; the header injection is correctly layered and all changed auth paths are covered by new tests.

The transport wrapping order is sound — quotaProjectTransport sits above oauth2.Transport so the X-Goog-User-Project header passes through the oauth2 clone intact. The one design choice worth a second look is using the service-account's project_id field as the default quota project: that field identifies the SA's home project, not an explicit billing override, and could produce unexpected header values in cross-project SA deployments. Everything else — FindDefaultCredentials migration, JSON fallback parsing for ADC, backward-compatible TokenSource wrapper, and test coverage — looks correct.

googleauth.go — specifically the resolveQuotaProject comment around SA project_id semantics vs. an explicit quota-project override.

Important Files Changed

Filename Overview
internal/providers/googleauth/googleauth.go Core change: adds FindCredentials(), resolveQuotaProject(), and quotaProjectTransport; HTTPClient gains a quotaProject parameter. Transport wrapping order is correct — quotaProjectTransport is outermost so the header survives oauth2.Transport's internal clone.
internal/providers/vertex/vertex.go Switches from TokenSource to FindCredentials, then prefers creds.QuotaProjectID and falls back to VertexProject for the X-Goog-User-Project header.
internal/providers/gemini/gemini.go Mirrors the same FindCredentials + quota project fallback pattern as vertex.go. authHTTPClient is only reachable for GCP-authed Vertex-backend Gemini, so the VertexProject fallback is appropriate.
internal/providers/googleauth/googleauth_test.go Adds four new unit tests covering ADC quota_project_id round-trip, SA project_id round-trip, header presence, and header omission using httptest servers.
internal/providers/vertex/vertex_test.go Adds TestNewSetsQuotaProjectHeaderOnVertexRequests with two table-driven sub-cases verifying credential-wins and VERTEX_PROJECT-fallback scenarios end-to-end.
internal/providers/gemini/gemini_test.go Minimal change: adds the empty-string quota project argument to the HTTPClient call in the Vertex test helper to match the updated signature.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant quotaProjectTransport
    participant oauth2Transport as oauth2.Transport
    participant BaseTransport
    participant GoogleAPI as Google API

    Caller->>quotaProjectTransport: RoundTrip(req)
    quotaProjectTransport->>quotaProjectTransport: "req.Clone() -> set X-Goog-User-Project"
    quotaProjectTransport->>oauth2Transport: RoundTrip(clonedReq)
    oauth2Transport->>oauth2Transport: "req.Clone() -> set Authorization: Bearer token"
    oauth2Transport->>BaseTransport: RoundTrip(finalReq)
    BaseTransport->>GoogleAPI: HTTP request (X-Goog-User-Project + Authorization)
    GoogleAPI-->>BaseTransport: Response
    BaseTransport-->>oauth2Transport: Response
    oauth2Transport-->>quotaProjectTransport: Response
    quotaProjectTransport-->>Caller: Response
Loading

Reviews (1): Last reviewed commit: "fix(providers): propagate quota project ..." | Re-trigger Greptile

Comment on lines +99 to +112
// resolveQuotaProject returns the project that should be sent as
// X-Goog-User-Project. For service-account credentials this is the `project_id`
// already surfaced on *google.Credentials. For authorized-user ADC the public
// Credentials struct leaves ProjectID empty, so we parse `quota_project_id`
// from the raw JSON ourselves — that field is what `gcloud auth
// application-default set-quota-project` writes and is required for ADC
// access to APIs like aiplatform.googleapis.com.
func resolveQuotaProject(creds *google.Credentials) string {
if creds == nil {
return ""
}
if project := strings.TrimSpace(creds.ProjectID); project != "" {
return project
}
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 Service account project_id used as quota project by default. For service accounts, creds.ProjectID is the project that owns the SA, not necessarily a billing override. Google's own client libraries typically omit X-Goog-User-Project for SA credentials and let IAM derive the billing project from the SA's permissions. Sending the SA's home project works in single-project setups, but in cross-project configurations (SA from project A, resources in project B) this could send an unexpected value. Operators can override via quota_project_id in their credential config, but this implicit default may be surprising. Consider documenting the fallback behavior more explicitly in the resolveQuotaProject comment.

Suggested change
// resolveQuotaProject returns the project that should be sent as
// X-Goog-User-Project. For service-account credentials this is the `project_id`
// already surfaced on *google.Credentials. For authorized-user ADC the public
// Credentials struct leaves ProjectID empty, so we parse `quota_project_id`
// from the raw JSON ourselves — that field is what `gcloud auth
// application-default set-quota-project` writes and is required for ADC
// access to APIs like aiplatform.googleapis.com.
func resolveQuotaProject(creds *google.Credentials) string {
if creds == nil {
return ""
}
if project := strings.TrimSpace(creds.ProjectID); project != "" {
return project
}
// resolveQuotaProject returns the project that should be sent as
// X-Goog-User-Project. For authorized-user ADC the public Credentials struct
// leaves ProjectID empty, so we parse `quota_project_id` from the raw JSON
// ourselves — that field is what `gcloud auth application-default
// set-quota-project` writes and is required for ADC access to APIs like
// aiplatform.googleapis.com. For service-account credentials `project_id` is
// the SA's home project; callers in cross-project setups should override via
// VertexProject or by adding quota_project_id to the SA JSON.
func resolveQuotaProject(creds *google.Credentials) string {
if creds == nil {
return ""
}
if project := strings.TrimSpace(creds.ProjectID); project != "" {
return project
}

Comment on lines +135 to +138
quotaProject := creds.QuotaProjectID
if strings.TrimSpace(quotaProject) == "" {
quotaProject = strings.TrimSpace(providerCfg.VertexProject)
}
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 Duplicated quota-project fallback logic. The same five-line pattern (prefer creds.QuotaProjectID, fall back to VertexProject) appears identically in both vertex.go and gemini.go. If the fallback priority ever changes, it will need updating in two places. CLAUDE.md's KISS guidance makes this acceptable now, but a note to keep the two in sync would reduce future drift.

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