JuliusBrussee/caveman · error

gemini bearer credential requires a valid x-goog-user-projec

Error message

gemini bearer credential requires a valid x-goog-user-project

What it means

For Gemini with a bearer-scheme credential (Gemini CLI OAuth/ADC), the adapter must send x-goog-user-project so Google attributes the call to the caller's quota/billing project. The code refuses to guess: if the inbound x-goog-user-project header is absent or fails validGoogleQuotaProject, the request is rejected. Remapping a bearer token to x-goog-api-key instead would break auth and mis-attribute quota, so this is a correctness fail-closed guard.

Source

Thrown at proxy/providers/adapter.go:476

		if out.Get("anthropic-version") == "" {
			out.Set("anthropic-version", "2023-06-01")
		}
	case "gemini":
		if credential.Key != "" {
			if credential.Scheme == "bearer" {
				// Standalone env fallback uses this synthetic marker to reach the
				// post-sanitization key resolver. It is not OAuth and must not be
				// forwarded or subjected to OAuth quota-project validation.
				if credential.Key == "no-key-required" {
					break
				}
				// Gemini CLI OAuth/ADC credentials are bearer tokens. Remapping one to
				// x-goog-api-key breaks authentication and silently defeats OAuth wrap.
				// Google also requires the caller's quota project for user OAuth. Never
				// guess it: wrong attribution is a billing and quota correctness bug.
				quotaProject := strings.TrimSpace(req.Header.Get("x-goog-user-project"))
				if !validGoogleQuotaProject(quotaProject) {
					return nil, fmt.Errorf("gemini bearer credential requires a valid x-goog-user-project")
				}
				out.Set("authorization", "Bearer "+credential.Key)
				out.Set("x-goog-user-project", quotaProject)
			} else {
				out.Set("x-goog-api-key", credential.Key)
			}
		}
	case "vertex":
		if credential.Key != "" {
			out.Set("authorization", "Bearer "+credential.Key)
		}
		copyIfPresent(out, req.Header, "x-vertex-ai-llm-request-type")
		copyIfPresent(out, req.Header, "x-goog-user-project")
	case "azure_openai":
		if credential.Key != "" {
			// The standalone placeholder is a synthetic no-key marker used by
			// callers that explicitly allow env-backed API-key fallback. It is
			// not an Entra token and must not be forwarded as Authorization;

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Have the client send a valid quota project header: x-goog-user-project: my-gcp-project on Gemini OAuth requests through the proxy.
  2. If you never intended OAuth, configure an API-key credential instead — the API-key branch sets x-goog-api-key and never hits this check.
  3. Ensure intermediaries forward the x-goog-user-project header (check droplists) and that its value is a real project id.

Example fix

# before
curl http://proxy:8080/gemini/v1/... -H "authorization: Bearer $OAUTH_TOKEN"
# -> error: bearer credential requires x-goog-user-project

# after
curl http://proxy:8080/gemini/v1/... \
  -H "authorization: Bearer $OAUTH_TOKEN" \
  -H "x-goog-user-project: my-gcp-project"
Defensive patterns

Strategy: validation

Validate before calling

// Before sending a Gemini OAuth request through the proxy:
qp := strings.TrimSpace(req.Header.Get("x-goog-user-project"))
if qp == "" || strings.ContainsAny(qp, " /") {
    return errors.New("Gemini OAuth requests need a valid x-goog-user-project header")
}
// prefer a real project id: letters, digits, hyphens
if !projectIDRe.MatchString(qp) {
    return fmt.Errorf("x-goog-user-project %q is not a valid project id", qp)
}

Type guard

var projectIDRe = regexp.MustCompile(`^[a-z][a-z0-9-]{4,28}[a-z0-9]$`)

func hasValidQuotaProject(h http.Header) bool {
    v := strings.TrimSpace(h.Get("x-goog-user-project"))
    return projectIDRe.MatchString(v)
}

Try / catch

if _, err := adapter.BuildUpstreamHeaders(credential, req); err != nil {
    if strings.Contains(err.Error(), "x-goog-user-project") {
        http.Error(w, "Gemini OAuth requires the x-goog-user-project header (your GCP quota project)", http.StatusBadRequest)
        return
    }
    http.Error(w, err.Error(), http.StatusBadRequest)
}

Prevention

When it happens

Trigger: Routing Gemini CLI (OAuth) traffic through the proxy without the client sending x-goog-user-project; the header present but empty, whitespace-padded, or malformed so validation fails; using an ADC/bearer credential where an API-key credential was intended.

Common situations: Switching a client from an API key to OAuth login and forgetting quota-project config; the header name typo'd by an intermediate proxy; GOOGLE_CLOUD_PROJECT set in env but the header never attached to proxied requests.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/746ec5fb97d7d48e. Report an issue: GitHub.