Skip to content

feat(rust): port VertexBase auth (Google credential resolution + token) to litellm-core as a base provider - #33906

Open
devin-ai-integration[bot] wants to merge 3 commits into
litellm_internal_stagingfrom
litellm_rust_vertex_auth
Open

feat(rust): port VertexBase auth (Google credential resolution + token) to litellm-core as a base provider#33906
devin-ai-integration[bot] wants to merge 3 commits into
litellm_internal_stagingfrom
litellm_rust_vertex_auth

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

This is PR 2 of the Rust migration's auth de-risking; PR 1 (#33888, merged) did the same for Bedrock's boto3/botocore auth. The question here is whether the google-auth auth surface that the Python Vertex path depends on has a maintained Rust equivalent that behaves the same, so the Rust gateway can resolve Google credentials and mint access tokens natively instead of calling back into Python. It does: Google's official google-cloud-auth crate (from googleapis/google-cloud-rust)

Nothing is wired into a live route yet and the whole module is behind a default-off vertex-auth cargo feature, so there is no proxy endpoint to curl at this stage; core still builds and clippies clean with the feature off. A live token call is not included because there are no Google credentials provisioned in this environment, unlike the AWS keys used for PR 1. The proof at this stage is the pure credential-source classifier and the credential/project resolution, pinned by unit tests that assert the exact Python load_auth dispatch order and the project-id precedence; both fail if the branch order or precedence drifts

cd litellm-rust && cargo test -p litellm-core --features vertex-auth
# 83 passed

Captured at commit e0f2e5e

Type

🆕 New Feature

Changes

Ports the auth surface of Python's VertexBase (litellm/llms/vertex_ai/vertex_llm_base.py) into the Rust workspace as a base provider at litellm-core/src/providers/vertex_ai/vertex_base.rs, so the Rust Vertex path can resolve Google credentials and obtain an access token natively. It is gated behind a new, default-off vertex-auth cargo feature on litellm-core; the google-cloud-auth dependency is optional, pinned to =1.12.0, and rustls-only (default-features = false, default-rustls-provider), so no OpenSSL or native-tls is linked

Credential-source selection is a pure function over the parsed credentials JSON, preserving the same predicate order as load_auth, so it is unit-testable without the network

type == external_account, credential_source.environment_id contains "aws" -> AwsWorkloadIdentity
type == external_account, credential_source has "executable"               -> ExecutableWorkloadIdentity
type == external_account, otherwise                                        -> IdentityPoolWorkloadIdentity
type == authorized_user                                                    -> AuthorizedUser
otherwise (credentials present)                                            -> ServiceAccount
no credentials provided                                                    -> DefaultAdc

Credential input resolution mirrors Python: a string that names an existing file is read and parsed as JSON, otherwise the string is parsed as inline JSON, and an already-parsed object is passed through; empty or whitespace-only credentials are treated as absent, and non-object or malformed JSON returns a typed error rather than panicking. Project id follows Python's precedence: an explicit project wins, then the credential's project_id, then its quota_project_id, then GOOGLE_CLOUD_PROJECT, then GOOGLE_CLOUD_QUOTA_PROJECT. Tokens are requested with the cloud-platform scope

All six sources are handled by google-cloud-auth with no silent downgrade or faked parity: service account, authorized user, default ADC, and the three external_account workload-identity variants (AWS-sourced, executable/pluggable, and identity-pool), the last three built through the crate's external_account builder which dispatches on the credential JSON internally

Caching reuses the shared litellm-core InMemoryCache (added in PR 1) through a process-wide OnceLock<Mutex<InMemoryCache<AccessTokenCredentials>>>, keyed by a SHA-256 hash of the resolved credential JSON plus project id so raw credential material is never used as a key or logged. Python's sync/async single-flight and background-refresh machinery is intentionally not reimplemented: it exists to work around google-auth's sync refresh() not being concurrency-safe, whereas the Rust AccessTokenCredentials are async and own their token cache and refresh internally

Placement note for reviewers: reading credentials and minting tokens is auth work that the core-purity guidance in litellm-rust/CLAUDE.md normally keeps out of litellm-core. Putting the base provider here, mirroring how Python's base provider owns auth and matching the merged Bedrock PR, is a deliberate, directed decision; the module carries a short note to that effect and the purity guidance will be reconciled separately rather than silently

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/bdf99b89a6584cc0989b5605765aec03
Requested by: @ishaan-berri

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
@ishaan-berri ishaan-berri self-assigned this Jul 19, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR ports the Python VertexBase Google credential resolution and token-minting logic into the Rust workspace as litellm-core/src/providers/vertex_ai/vertex_base.rs, gated behind a default-off vertex-auth cargo feature. Nothing is wired to a live route yet. The credential-source classifier and project-ID precedence chain are unit-tested and match the Python behavior.

  • resolve_token cache error handling is fragile: cached_credentials(&cache_key)? and store_credentials(cache_key, provider.clone())? both propagate Mutex lock-poisoning as hard auth errors. A poisoned lock blocks the entire flow before credentials are even built, and discards a successfully-fetched token if poisoning happens after the network call. Both paths should degrade gracefully (treat lock-poisoning as a cache miss).
  • Clone semantics of AccessTokenCredentials affect token reuse: InMemoryCache::get_cache returns a clone() of the stored provider. If Clone is a value copy rather than Arc-backed, each caller holds an independent token snapshot and the 10-minute InMemoryCache TTL means refreshes are unbounded across concurrent callers; a comment clarifying the assumption would help.
  • A temp file created in the unit test is not cleaned up on assertion panic.

Confidence Score: 3/5

Safe to merge for now since the module is behind a default-off feature flag with no live route, but the cache error handling in resolve_token needs to be fixed before the feature is enabled in production.

The credential classifier and project-ID resolution are correct and well-tested. The process-wide singleton cache propagates Mutex lock-poisoning as a hard auth error at both the lookup and the store step — if the lock is ever poisoned, every subsequent resolve_token call fails permanently without attempting a network fetch, and a successfully-fetched token is silently discarded at the store step.

litellm-rust/crates/core/src/providers/vertex_ai/vertex_base.rs — specifically the cached_credentials and store_credentials call sites inside resolve_token.

Important Files Changed

Filename Overview
litellm-rust/crates/core/src/providers/vertex_ai/vertex_base.rs New Vertex AI base provider for Google credential resolution and token minting. The token fetch is not resilient to cache errors: a poisoned cache lock discards a successfully-fetched token and permanently blocks all future auth.
litellm-rust/crates/core/Cargo.toml Adds google-cloud-auth = "=1.12.0" pinned as an optional dep behind the new vertex-auth feature; existing workspace deps like sha2 are already present. Clean addition.
litellm-rust/crates/core/src/providers/vertex_ai/constants.rs New constants file defining all Google/Vertex environment variable names and the cloud-platform scope. Values are correct.
litellm-rust/crates/core/src/providers/vertex_ai/mod.rs Exposes vertex_base and constants modules behind #[cfg(feature = "vertex-auth")] guards. Correct and minimal.

Reviews (1): Last reviewed commit: "feat(rust): add Vertex auth base provide..." | Re-trigger Greptile

Comment on lines +145 to +164
None => {
let provider = build_credentials(source, credentials)?;
let access_token = provider.access_token().await.map_err(|error| {
CoreError::Auth(format!("Google token resolution failed: {error}"))
})?;
store_credentials(cache_key, provider.clone())?;
return Ok(VertexToken {
access_token: access_token.token,
project_id,
});
}
};
let access_token = provider
.access_token()
.await
.map_err(|error| CoreError::Auth(format!("Google token resolution failed: {error}")))?;
Ok(VertexToken {
access_token: access_token.token,
project_id,
})

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 Cache error fatally discards a valid access token

store_credentials(cache_key, provider.clone())? propagates a cache write failure (e.g. a poisoned Mutex) after access_token was already successfully fetched. The caller receives an auth error and must retry even though a perfectly good token was in hand. Worse, cached_credentials(&cache_key)? on line 137 fails at the lookup step too, meaning a poisoned lock prevents any credential building at all—every subsequent resolve_token call fails immediately without attempting a network fetch, which is unrecoverable without a process restart. Both helpers should treat lock-poisoning as a non-fatal cache miss rather than a hard error: lock().unwrap_or_else(|e| e.into_inner()), or simply let _ = store_credentials(...) so a valid token is always returned if the fetch succeeds.

Comment on lines +232 to +250
project_id.ok_or(CoreError::MissingField("project_id"))
}

fn cache_key(credentials: Option<&Value>, project_id: &str) -> String {
let mut hasher = Sha256::new();
if let Some(credentials) = credentials {
hasher.update(credentials.to_string());
}
hasher.update(project_id);
format!("{:x}", hasher.finalize())
}

fn cached_credentials(key: &str) -> CoreResult<Option<AccessTokenCredentials>> {
let cache = VERTEX_CREDENTIALS_CACHE.get_or_init(|| Mutex::new(InMemoryCache::default()));
let mut cache = cache
.lock()
.map_err(|_| CoreError::Auth("Vertex credential cache lock poisoned".to_string()))?;
Ok(cache.get_cache(key))
}

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 Clone semantics of AccessTokenCredentials determine cache effectiveness

InMemoryCache::get_cache returns a clone of the stored AccessTokenCredentials. If Clone on that type is a value copy (not Arc-backed shared state), each caller gets an independent snapshot of the credentials as they existed at storage time. After the internal token expires, every caller would independently re-fetch from Google rather than sharing a single refresh. Given the InMemoryCache default TTL is 10 minutes (vs. a 1-hour token lifetime), the cache's primary benefit reduces to avoiding credential-object rebuilds rather than avoiding token network calls. If the google-cloud-auth crate's AccessTokenCredentials uses Arc internally for its token cache, this is a non-issue—but that contract is implicit. A clarifying comment noting the assumption (or preferring an Arc<AccessTokenCredentials> in the cache) would make the intended behavior explicit.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +295 to +315
fn classifier_covers_authorized_service_and_default_sources() {
assert_eq!(
classify_auth(Some(&json!({"type": "authorized_user"}))),
VertexAuthSource::AuthorizedUser
);
assert_eq!(
classify_auth(Some(&json!({"type": "service_account"}))),
VertexAuthSource::ServiceAccount
);
assert_eq!(
classify_auth(Some(&json!({"client_email": "test@example.com"}))),
VertexAuthSource::ServiceAccount
);
assert_eq!(classify_auth(None), VertexAuthSource::DefaultAdc);
}

#[test]
fn credential_input_reads_files_inline_json_and_objects() {
let path = std::env::temp_dir().join(format!("vertex-auth-{}.json", std::process::id()));
std::fs::write(&path, r#"{"type":"authorized_user"}"#).unwrap();
assert_eq!(

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 Temp file not cleaned up on test panic

std::fs::remove_file(path).unwrap() is called unconditionally after the first assertion, but if the assert_eq! on line 304 panics the file is left in the temp directory. Wrapping the body in a scope with a defer-style guard (or using a Drop wrapper) would ensure cleanup even on assertion failure.

@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_rust_vertex_auth (fa46ad7) with litellm_internal_staging (24b55d2)

Open in CodSpeed

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot requested a review from a team July 19, 2026 02:39
Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
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