fix(providers): propagate quota project to Vertex via X-Goog-User-Project#325
Conversation
…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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughThe PR implements quota-project support for Google Cloud authentication. A new ChangesQuota-Project Support across Providers
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR fixes a live ADC regression on Vertex AI by propagating the quota project through a new
Confidence Score: 4/5Safe 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
Sequence DiagramsequenceDiagram
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
Reviews (1): Last reviewed commit: "fix(providers): propagate quota project ..." | Re-trigger Greptile |
| // 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 | ||
| } |
There was a problem hiding this comment.
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.
| // 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 | |
| } |
| quotaProject := creds.QuotaProjectID | ||
| if strings.TrimSpace(quotaProject) == "" { | ||
| quotaProject = strings.TrimSpace(providerCfg.VertexProject) | ||
| } |
There was a problem hiding this comment.
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.
Summary
internal/providers/googleauthfromgoogle.DefaultTokenSource/google.JWTConfigFromJSONtogoogle.FindDefaultCredentials/google.CredentialsFromJSONWithType, and expose a newCredentialswrapper that carries the resolved quota project alongside the token source.quota_project_idfrom the raw ADC JSON ourselves —*google.Credentials.ProjectIDonly surfaces SAproject_id, the authorized-user ADC quota override never propagates through the public struct.quotaProjectparameter togoogleauth.HTTPClient. When non-empty, a smallRoundTripperwrapper setsX-Goog-User-Projecton every outbound request (above the existing OAuth2 token transport).VERTEX_PROJECT. Single-project deployments work out-of-the-box withoutgcloud auth application-default set-quota-project.TokenSource()is kept as a thin wrapper aroundFindCredentialsfor callers that only need the token source.Why
Surfaced during live testing of #324. With ADC user credentials (the common
gcloud auth application-default loginflow) GoModel was sending requests toaiplatform.googleapis.comwithout anX-Goog-User-Projectheader. Google's Vertex API responds: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 inapplication_default_credentials.jsonand was never read.Test plan
make test-race,make lint,go mod tidy,mint validate— all green via pre-commit hooks.googleauthtests:TestFindCredentialsReadsADCQuotaProject— ADC user JSON withquota_project_idround-trips throughFindCredentials.TestFindCredentialsReadsServiceAccountProject— SA JSON withproject_idround-trips throughFindCredentials.TestHTTPClientSetsQuotaProjectHeader— upstream receivesX-Goog-User-Project: <project>when set.TestHTTPClientOmitsQuotaProjectHeaderWhenEmpty— header is omitted entirely when not set.vertexintegration testTestNewSetsQuotaProjectHeaderOnVertexRequestswith a livehttptestupstream:quota_project_id=billing-project+VERTEX_PROJECT=prod-ai→ upstream seesX-Goog-User-Project: billing-project(credential wins).quota_project_id+VERTEX_PROJECT=prod-ai→ upstream seesX-Goog-User-Project: prod-ai(fallback).googleauth,vertex, andgeminitests updated for the newHTTPClientsignature; 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.jsonwill start seeing it propagated automatically; users without one fall back toVERTEX_PROJECT, which has always been required for the Vertex provider anyway.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Tests