From 268e8bb735b6871bfed8e593be1b0b53e277d949 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 14:53:12 -0700 Subject: [PATCH] refactor(rust): share anthropic types, request helpers, and streaming contracts across crates (#43426) * refactor(rust): standardize Azure Messages module path * docs(rust): define shared types crate boundaries * refactor(rust): share request helpers and type Anthropic blocks * docs(rust): format shared type invariants as bullets * test(rust): parameterize repeated cases with rstest * refactor(rust): move Responses transform result into llms * fix(anthropic): validate chat and batch responses * docs(rust): clarify API format ownership boundaries * docs: clarify Rust error message construction * refactor(auth): keep shared Rust errors provider-neutral * refactor(rust): separate format contracts from provider policy * fix(rust): type Anthropic chat response text collection * fix(rust): pass audio secret sources through hosts * fix(rust): unblock batch lint and OCR error assertions * test(rust): assert response failures at the adapter boundary * refactor(rust): declare error messages with typed context * wip * fix(rust): adapt Bedrock error details * style(rust): cargo fmt bedrock audio transcription Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(rust): adapt tests and dead code to typed error details Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(rust): keep converse error contracts and read env secrets without litellm Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ci(rust): raise the native wheel size gate to 45 MB Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(rust): tolerate missing usage in converse responses on the transcription route Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Yujong Lee Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/scripts/verify_linux_native_wheel.py | 2 +- litellm-rust/AGENTS.md | 2 + litellm-rust/Cargo.lock | 10 +- litellm-rust/crates/auth-azure/src/native.rs | 74 ++-- litellm-rust/crates/auth-azure/src/resolve.rs | 70 +++- litellm-rust/crates/auth-azure/src/types.rs | 7 +- litellm-rust/crates/auth-gcp/Cargo.toml | 3 + litellm-rust/crates/auth-gcp/src/lib.rs | 51 +-- litellm-rust/crates/auth-types/Cargo.toml | 1 + .../crates/auth-types/src/credential.rs | 16 +- litellm-rust/crates/auth-types/src/error.rs | 173 +++++----- litellm-rust/crates/auth-types/src/http.rs | 8 +- litellm-rust/crates/auth-types/src/lib.rs | 2 +- litellm-rust/crates/auth-types/src/policy.rs | 10 +- litellm-rust/crates/auth-types/tests/error.rs | 74 ++++ .../crates/core-utils/src/call_arguments.rs | 34 +- .../crates/core-utils/src/core_helpers.rs | 34 +- .../crates/core-utils/src/serde_compat.rs | 29 +- .../crates/core-utils/src/settings.rs | 16 + .../crates/core-utils/src/url_utils.rs | 24 +- .../crates/core-utils/tests/settings.rs | 33 ++ litellm-rust/crates/core/AGENTS.md | 2 + litellm-rust/crates/core/Cargo.toml | 2 - .../core/src/audio_transcription/handler.rs | 10 +- .../core/src/audio_transcription/mod.rs | 4 +- .../core/src/audio_transcription/prepare.rs | 13 +- .../core/src/audio_transcription/types.rs | 2 + .../core/src/chat_completions/handler.rs | 13 +- .../crates/core/src/chat_completions/mod.rs | 6 +- .../core/src/chat_completions/prepare.rs | 39 ++- .../crates/core/src/chat_completions/types.rs | 2 + litellm-rust/crates/core/src/error.rs | 35 +- .../crates/core/src/messages/AGENTS.md | 7 + .../crates/core/src/messages/common_utils.rs | 4 +- .../crates/core/src/messages/handler.rs | 19 +- .../crates/core/src/messages/prepare.rs | 17 +- .../crates/core/src/messages/route.rs | 2 +- .../crates/core/src/messages/types.rs | 17 +- litellm-rust/crates/core/src/ocr/document.rs | 34 +- litellm-rust/crates/core/src/ocr/route.rs | 2 +- .../crates/core/src/responses/websocket.rs | 82 +---- .../crates/core/tests/audio_transcription.rs | 71 +++- .../crates/core/tests/chat_completions.rs | 169 +++++++++- .../crates/core/tests/messages/host.rs | 6 +- .../crates/core/tests/messages/request.rs | 126 +++++-- .../crates/core/tests/messages/response.rs | 2 +- .../crates/core/tests/messages/secrets.rs | 8 +- .../crates/core/tests/ocr/azure_ai.rs | 4 +- litellm-rust/crates/cost/Cargo.toml | 1 + litellm-rust/crates/cost/tests/calculation.rs | 28 +- .../src/audio_transcription.rs | 1 + .../gateway-inference/src/chat_completions.rs | 1 + litellm-rust/crates/host/src/machine/auth.rs | 2 +- litellm-rust/crates/http/AGENTS.md | 6 + litellm-rust/crates/http/Cargo.toml | 3 + litellm-rust/crates/http/src/lib.rs | 1 + litellm-rust/crates/http/src/media.rs | 48 +-- litellm-rust/crates/http/src/request.rs | 34 +- litellm-rust/crates/http/src/websocket.rs | 61 ++++ litellm-rust/crates/http/tests/request.rs | 69 ++++ litellm-rust/crates/http/tests/websocket.rs | 59 ++++ litellm-rust/crates/llms/AGENTS.md | 22 +- .../crates/llms/src/anthropic/AGENTS.md | 8 + .../src/anthropic/batches/transformation.rs | 64 +++- .../crates/llms/src/anthropic/chat/handler.rs | 23 +- .../llms/src/anthropic/chat/transformation.rs | 97 ++++-- .../crates/llms/src/anthropic/common_utils.rs | 319 +++++++----------- .../llms/src/anthropic/messages/AGENTS.md | 10 +- .../llms/src/anthropic/messages/handler.rs | 27 +- .../crates/llms/src/anthropic/messages/mod.rs | 1 - .../llms/src/anthropic/messages/thinking.rs | 205 +++++------ .../src/anthropic/messages/transformation.rs | 160 ++++----- .../crates/llms/src/azure_ai/anthropic/mod.rs | 1 - .../crates/llms/src/azure_ai/common_utils.rs | 25 ++ .../llms/src/azure_ai/messages/AGENTS.md | 3 + .../messages}/mod.rs | 1 - .../transformation.rs} | 191 ++++------- litellm-rust/crates/llms/src/azure_ai/mod.rs | 3 +- .../llms/src/azure_ai/ocr/common_utils.rs | 7 +- .../llms/src/azure_ai/ocr/transformation.rs | 6 +- .../audio_transcription/transformation.rs | 18 +- litellm-rust/crates/llms/src/base_llm/auth.rs | 45 +-- .../llms/src/base_llm/chat/transformation.rs | 2 + .../llms/src/base_llm/messages/AGENTS.md | 5 + .../llms/src/base_llm/messages/context.rs | 134 ++++++++ .../crates/llms/src/base_llm/messages/mod.rs | 4 + .../src/base_llm/messages/normalization.rs | 50 +++ .../streaming.rs | 47 ++- .../transformation.rs | 50 ++- litellm-rust/crates/llms/src/base_llm/mod.rs | 2 +- .../src/base_llm/responses/transformation.rs | 101 +----- .../src/bedrock/audio_transcription/mod.rs | 105 ++++-- .../bedrock/chat/converse_transformation.rs | 170 ++++++++-- .../llms/src/bedrock/chat/invoke_handler.rs | 66 ++-- .../llms/src/bedrock/messages/AGENTS.md | 3 + .../anthropic_claude3_transformation.rs | 83 ++--- litellm-rust/crates/llms/src/error.rs | 126 ++++++- litellm-rust/crates/llms/src/lib.rs | 2 +- .../src/openai/responses/transformation.rs | 94 +++++- .../src/openai_like/chat/transformation.rs | 4 + .../llms/src/openai_like/common_utils.rs | 2 +- .../llms/src/vertex_ai/ocr/common_utils.rs | 5 +- .../tests/anthropic_chat_transformation.rs | 61 ++-- .../tests/bedrock_converse_transformation.rs | 78 +++-- .../llms/tests/messages_normalization.rs | 48 +++ .../tests/openai_like_chat_transformation.rs | 2 +- .../crates/python-bridge/src/coercion.rs | 49 ++- .../crates/python-bridge/src/credentials.rs | 45 +-- .../crates/python-bridge/src/errors.rs | 4 +- .../src/routes/audio_transcription.rs | 20 +- .../src/routes/chat_completions.rs | 6 + .../python-bridge/src/routes/messages/host.rs | 4 +- .../python-bridge/src/secrets/config.rs | 10 +- .../crates/python-bridge/src/secrets/mod.rs | 8 +- .../src/secret_manager/client.rs | 25 +- .../crates/token-counter-fast/src/error.rs | 16 +- .../crates/token-counter-fast/src/lib.rs | 2 +- .../crates/token-counter-fast/src/tiktoken.rs | 31 +- .../token-counter-huggingface/Cargo.toml | 3 + .../token-counter-huggingface/src/lib.rs | 18 +- .../crates/token-counter-tiktoken/Cargo.toml | 3 + .../crates/token-counter-tiktoken/src/lib.rs | 49 ++- .../token-counter-tiktoken/src/ranks.rs | 19 +- .../crates/token-counter/src/error.rs | 2 +- litellm-rust/crates/token-counter/src/fast.rs | 2 +- .../crates/token-counter/src/tiktoken.rs | 2 +- litellm-rust/crates/types/AGENTS.md | 60 ++++ .../crates/types/src/audio_transcription.rs | 15 + litellm-rust/crates/types/src/lib.rs | 2 + .../anthropic_messages/anthropic_request.rs | 53 ++- .../crates/types/src/messages/AGENTS.md | 5 + litellm-rust/crates/types/src/messages/mod.rs | 1 + .../src/messages/streaming.rs} | 30 +- .../src/responses/streaming_websocket.rs | 79 ++--- .../crates/types/tests/anthropic_request.rs | 49 +++ .../crates/types/tests/messages_streaming.rs | 37 ++ 136 files changed, 3199 insertions(+), 1615 deletions(-) create mode 100644 litellm-rust/crates/auth-types/tests/error.rs create mode 100644 litellm-rust/crates/core-utils/tests/settings.rs create mode 100644 litellm-rust/crates/core/src/messages/AGENTS.md create mode 100644 litellm-rust/crates/http/src/websocket.rs create mode 100644 litellm-rust/crates/http/tests/request.rs create mode 100644 litellm-rust/crates/http/tests/websocket.rs create mode 100644 litellm-rust/crates/llms/src/anthropic/AGENTS.md delete mode 100644 litellm-rust/crates/llms/src/azure_ai/anthropic/mod.rs create mode 100644 litellm-rust/crates/llms/src/azure_ai/common_utils.rs create mode 100644 litellm-rust/crates/llms/src/azure_ai/messages/AGENTS.md rename litellm-rust/crates/llms/src/{base_llm/anthropic_messages => azure_ai/messages}/mod.rs (55%) rename litellm-rust/crates/llms/src/azure_ai/{anthropic/messages_transformation.rs => messages/transformation.rs} (80%) create mode 100644 litellm-rust/crates/llms/src/base_llm/messages/AGENTS.md create mode 100644 litellm-rust/crates/llms/src/base_llm/messages/context.rs create mode 100644 litellm-rust/crates/llms/src/base_llm/messages/mod.rs create mode 100644 litellm-rust/crates/llms/src/base_llm/messages/normalization.rs rename litellm-rust/crates/llms/src/base_llm/{anthropic_messages => messages}/streaming.rs (67%) rename litellm-rust/crates/llms/src/base_llm/{anthropic_messages => messages}/transformation.rs (75%) create mode 100644 litellm-rust/crates/llms/src/bedrock/messages/AGENTS.md create mode 100644 litellm-rust/crates/llms/tests/messages_normalization.rs create mode 100644 litellm-rust/crates/types/AGENTS.md create mode 100644 litellm-rust/crates/types/src/audio_transcription.rs create mode 100644 litellm-rust/crates/types/src/messages/AGENTS.md create mode 100644 litellm-rust/crates/types/src/messages/mod.rs rename litellm-rust/crates/{llms/src/anthropic/messages/streaming_iterator.rs => types/src/messages/streaming.rs} (87%) create mode 100644 litellm-rust/crates/types/tests/anthropic_request.rs create mode 100644 litellm-rust/crates/types/tests/messages_streaming.rs diff --git a/.github/scripts/verify_linux_native_wheel.py b/.github/scripts/verify_linux_native_wheel.py index 6b7fcd57bbc..465918f5a81 100644 --- a/.github/scripts/verify_linux_native_wheel.py +++ b/.github/scripts/verify_linux_native_wheel.py @@ -214,7 +214,7 @@ def main( native_module: Final = load_native_module(native_path) native_module_loads: Final = native_module is not None panic_test_hook_absent: Final = native_module is not None and not hasattr(native_module, "_panic_for_test") - native_size_limit: Final = 40_000_000 + native_size_limit: Final = 45_000_000 native_size_within_limit: Final = native_member.file_size <= native_size_limit validations: Final = ( (f"Python tag is {EXPECTED_PYTHON_TAG}", python_tag == EXPECTED_PYTHON_TAG), diff --git a/litellm-rust/AGENTS.md b/litellm-rust/AGENTS.md index bc6a2552e4c..b1dc35d3698 100644 --- a/litellm-rust/AGENTS.md +++ b/litellm-rust/AGENTS.md @@ -16,7 +16,9 @@ Use [`#[rstest]`](https://docs.rs/rstest/latest/rstest/attr.rstest.html) for new ## Error definitions - A crate's errors live in `src/error.rs`, defined with `thiserror`, and re-exported from `lib.rs` +- Put message templates in the variant's `#[error(...)]` declaration. Callers pass only the small typed arguments needed to fill them, never `Error::Variant(format!(...))` or a preformatted message. Keep the smallest set of neutral variants that callers need to distinguish; different wording or providers do not justify new variants - Default to one top-level `Error` enum per crate, with one variant per failure mode and a `#[error(...)]` message on each. A failure mode is something a caller handles differently (phase, status code, retry, a message Python parity pins exactly); failures no caller tells apart share one variant and differ only in its message +- Keep shared error enums minimal and provider-neutral. Provider names, credential types, configuration fields, and setup guidance belong in caller-supplied data, not dedicated variants or hardcoded shared messages. Reuse a variant for the same failure mode across providers, such as `MissingApiBase { provider: "Azure", guidance: "..." }`. An exact parity message does not justify a provider-specific variant when caller-supplied context can preserve it - Wrap a lower-level error as a variant with `#[from]` or `#[source]` instead of flattening it to a string - Exception: split into separate types when different functions fail in disjoint ways, especially when different callers see them. A shared enum would force every caller to match variants its function can never return - Name a split type after what went wrong (a unit struct is fine for a single failure mode), not after the function that returns it diff --git a/litellm-rust/Cargo.lock b/litellm-rust/Cargo.lock index d67623feffd..bee4421f1e9 100644 --- a/litellm-rust/Cargo.lock +++ b/litellm-rust/Cargo.lock @@ -2891,6 +2891,7 @@ dependencies = [ "http 1.4.2", "litellm-auth-types", "moka", + "rstest", "serde_json", "sha2 0.10.9", "tokio", @@ -2900,6 +2901,7 @@ dependencies = [ name = "litellm-auth-types" version = "0.1.0" dependencies = [ + "rstest", "serde", "subtle", "thiserror 2.0.19", @@ -3146,8 +3148,6 @@ dependencies = [ "reqwest 0.12.28", "rstest", "rstest_reuse", - "rustls 0.23.42", - "rustls-native-certs", "serde", "serde_json", "sha2 0.10.9", @@ -3194,6 +3194,7 @@ version = "0.1.0" dependencies = [ "criterion", "proptest", + "rstest", ] [[package]] @@ -3305,6 +3306,7 @@ dependencies = [ name = "litellm-http" version = "0.1.0" dependencies = [ + "futures-util", "http 1.4.2", "hyper-util", "litellm-core-utils", @@ -3312,11 +3314,13 @@ dependencies = [ "reqwest 0.12.28", "rstest", "rustls 0.23.42", + "rustls-native-certs", "serde", "serde_json", "tempfile", "thiserror 2.0.19", "tokio", + "tokio-tungstenite", "veil", "webpki-roots", ] @@ -3661,6 +3665,7 @@ dependencies = [ name = "litellm-token-counter-huggingface" version = "0.1.0" dependencies = [ + "rstest", "serde_json", "thiserror 2.0.19", "tokenizers", @@ -3672,6 +3677,7 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "once_cell", + "rstest", "rustc-hash", "thiserror 2.0.19", "tiktoken-rs", diff --git a/litellm-rust/crates/auth-azure/src/native.rs b/litellm-rust/crates/auth-azure/src/native.rs index d635e559641..64752162384 100644 --- a/litellm-rust/crates/auth-azure/src/native.rs +++ b/litellm-rust/crates/auth-azure/src/native.rs @@ -133,7 +133,7 @@ impl NativeAzureTokenAcquirer { let token = credential .get_token(&[scope.as_str()], None) .await - .map_err(|error| Error::AzureTokenAcquisition(error.to_string()))?; + .map_err(|error| Error::CredentialAcquisition(error.to_string().into()))?; let expires_on = u64::try_from(token.expires_on.unix_timestamp()) .ok() .map(|seconds| UNIX_EPOCH + Duration::from_secs(seconds)); @@ -250,7 +250,12 @@ fn validate_authority(request: &NativeAzureRequest) -> Result<(), Error> { let Some(authority) = authority else { return Ok(()); }; - let url = url::Url::parse(authority.value()).map_err(|_| Error::InvalidAzureAuthority)?; + let url = url::Url::parse(authority.value()).map_err(|_| { + Error::InvalidConfiguration( + "Azure authority must be an HTTPS origin without credentials, query, or fragment" + .into(), + ) + })?; if url.scheme() != "https" || url.host_str().is_none() || !url.username().is_empty() @@ -259,7 +264,10 @@ fn validate_authority(request: &NativeAzureRequest) -> Result<(), Error> { || url.fragment().is_some() || !matches!(url.path(), "" | "/") { - return Err(Error::InvalidAzureAuthority); + return Err(Error::InvalidConfiguration( + "Azure authority must be an HTTPS origin without credentials, query, or fragment" + .into(), + )); } Ok(()) } @@ -368,7 +376,9 @@ fn trusted_source(sources: &[InputSource]) -> InputSource { } fn mixed_sources() -> Result { - Err(Error::MixedAzureCredentialSources) + Err(Error::InvalidConfiguration( + "request-controlled Azure auth inputs cannot be combined with host credentials".into(), + )) } fn build_credential( @@ -433,7 +443,12 @@ fn build_credential( NativeAzureRequest::DeveloperTools { .. } => DeveloperToolsCredential::new(None) .map(|credential| credential as Arc), } - .map_err(|error| Error::AzureCredentialInitialization(error.to_string())) + .map_err(|error| { + Error::InvalidConfiguration(litellm_auth_types::ErrorDetail::failed( + "Azure credential initialization", + error, + )) + }) } fn client_options( @@ -638,7 +653,7 @@ mod tests { assert_eq!(transport.requests.lock().unwrap().len(), 6); } - #[test] + #[rstest::rstest] fn request_authority_requires_request_owned_client_secret_identity() { let error = ValidatedAzureRequest::new(sourced_client_secret( InputSource::Deployment, @@ -647,10 +662,13 @@ mod tests { )) .unwrap_err(); - assert!(matches!( + assert_eq!( error, - litellm_auth_types::Error::MixedAzureCredentialSources - )); + litellm_auth_types::Error::InvalidConfiguration( + "request-controlled Azure auth inputs cannot be combined with host credentials" + .into() + ) + ); } #[test] @@ -665,24 +683,24 @@ mod tests { assert_eq!(request.credential_source(), InputSource::Request); } - #[test] - fn authority_is_restricted_to_an_https_origin() { - for authority in [ - "http://login.example", - "https://user@login.example", - "https://login.example/tenant", - "https://login.example?target=other", - ] { - let error = ValidatedAzureRequest::new(sourced_client_secret( - InputSource::Deployment, - InputSource::Deployment, - authority, - )) - .unwrap_err(); - assert!(matches!( - error, - litellm_auth_types::Error::InvalidAzureAuthority - )); - } + #[rstest::rstest] + #[case::http("http://login.example")] + #[case::userinfo("https://user@login.example")] + #[case::path("https://login.example/tenant")] + #[case::query("https://login.example?target=other")] + fn authority_is_restricted_to_an_https_origin(#[case] authority: &str) { + let error = ValidatedAzureRequest::new(sourced_client_secret( + InputSource::Deployment, + InputSource::Deployment, + authority, + )) + .unwrap_err(); + assert_eq!( + error, + litellm_auth_types::Error::InvalidConfiguration( + "Azure authority must be an HTTPS origin without credentials, query, or fragment" + .into() + ) + ); } } diff --git a/litellm-rust/crates/auth-azure/src/resolve.rs b/litellm-rust/crates/auth-azure/src/resolve.rs index 9a7afe645db..2142e22db50 100644 --- a/litellm-rust/crates/auth-azure/src/resolve.rs +++ b/litellm-rust/crates/auth-azure/src/resolve.rs @@ -91,7 +91,9 @@ impl AzureAuthService { AzureCredentialPlan::Caller(caller) => { let credential = caller.acquire().await?; if credential.secret().expose().is_empty() { - return Err(Error::EmptyAzureToken); + return Err(Error::EmptyCallerCredential( + "Azure AD token provider returned an empty token", + )); } Ok(Some(Sourced::new(credential, InputSource::Deployment))) } @@ -104,7 +106,11 @@ impl AzureAuthService { } => { let assertion = resolve_reference(inputs, env_lookup, reference.value()) .await? - .ok_or(Error::UnresolvedOidcReference)?; + .ok_or_else(|| { + Error::CredentialAcquisition( + "Azure OIDC reference did not resolve to a value".into(), + ) + })?; let request = ValidatedAzureRequest::new(NativeAzureRequest::ClientAssertion { tenant_id, client_id, @@ -167,7 +173,7 @@ pub(crate) fn select_auth_plan( .map(|selector| Sourced::new(selector, value.source())) }) .transpose() - .map_err(|_| Error::InvalidAzureSelector)?; + .map_err(|_| Error::InvalidConfiguration("invalid Azure credential selector".into()))?; let federated_token_file = configured_string( &inputs.federated_token_file, AZURE_FEDERATED_TOKEN_FILE_ENV, @@ -257,7 +263,9 @@ fn select_native_plan( let selection_source = selected.source(); match selected.into_value() { - AzureCredentialType::ClientSecretCredential => Err(Error::MissingClientSecretFields), + AzureCredentialType::ClientSecretCredential => Err(Error::InvalidConfiguration( + "ClientSecretCredential requires tenant_id, client_id, and client_secret".into(), + )), AzureCredentialType::WorkloadIdentityCredential => { Ok(AzureCredentialPlan::Native(ValidatedAzureRequest::new( workload_request(tenant_id, client_id, federated_token_file, scope, authority)?, @@ -341,9 +349,17 @@ fn workload_request( authority: Option>, ) -> Result { Ok(NativeAzureRequest::WorkloadIdentity { - tenant_id: tenant_id.ok_or(Error::MissingWorkloadTenant)?, - client_id: client_id.ok_or(Error::MissingWorkloadClient)?, - token_file_path: token_file_path.ok_or(Error::MissingWorkloadTokenFile)?, + tenant_id: tenant_id.ok_or_else(|| { + Error::InvalidConfiguration("WorkloadIdentityCredential requires tenant_id".into()) + })?, + client_id: client_id.ok_or_else(|| { + Error::InvalidConfiguration("WorkloadIdentityCredential requires client_id".into()) + })?, + token_file_path: token_file_path.ok_or_else(|| { + Error::InvalidConfiguration( + "WorkloadIdentityCredential requires azure_federated_token_file".into(), + ) + })?, scope, authority, }) @@ -394,10 +410,11 @@ async fn resolve_reference( .map_or(CredentialLookup::Missing, CredentialLookup::Found), CredentialRef::None => return Ok(None), CredentialRef::File(_) | CredentialRef::Request(_) | CredentialRef::Host(_) => { - let resolver = inputs - .credential_resolver - .as_ref() - .ok_or(Error::MissingHostResolver)?; + let resolver = inputs.credential_resolver.as_ref().ok_or_else(|| { + Error::InvalidConfiguration( + "credential reference requires a host credential resolver".into(), + ) + })?; resolver.resolve(reference).await? } }; @@ -415,7 +432,9 @@ fn oidc_reference( }; let value = token.value().expose(); if token.source() == InputSource::Request && value.starts_with("oidc/") { - return Err(Error::RequestAzureCredentialReference); + return Err(Error::InvalidConfiguration( + "request-controlled Azure credential references are not allowed".into(), + )); } if let Some(name) = value.strip_prefix("oidc/env/") { return non_empty_reference(name, "OIDC environment reference") @@ -437,14 +456,20 @@ fn oidc_reference( ))); } if value.starts_with("oidc/") { - return Err(Error::UnsupportedOidcReference); + return Err(Error::InvalidConfiguration( + "unsupported OIDC reference".into(), + )); } Ok(None) } fn non_empty_reference(value: &str, kind: &str) -> Result { if value.is_empty() { - return Err(Error::EmptyReference(kind.to_string())); + return Err(Error::InvalidConfiguration( + litellm_auth_types::ErrorDetail::Empty { + subject: kind.into(), + }, + )); } Ok(value.to_string()) } @@ -493,7 +518,7 @@ mod tests { expires_on: None, }) } else { - Err(Error::AzureTokenAcquisition(format!("{kind} failed"))) + Err(Error::CredentialAcquisition(kind.into())) } }) } @@ -602,7 +627,7 @@ mod tests { assert!(error.to_string().contains("unsupported OIDC reference")); } - #[test] + #[rstest::rstest] fn request_oidc_reference_is_rejected_before_lookup() { let params = json!({ "azure_ad_token": "oidc/env/ASSERTION", @@ -624,7 +649,12 @@ mod tests { }) .unwrap_err(); - assert!(matches!(error, Error::RequestAzureCredentialReference)); + assert_eq!( + error, + Error::InvalidConfiguration( + "request-controlled Azure credential references are not allowed".into() + ) + ); } #[tokio::test] @@ -723,6 +753,7 @@ mod tests { assert_eq!(credential.value().secret().expose(), "caller-token"); } + #[rstest::rstest] #[tokio::test] async fn empty_caller_token_is_rejected() { let error = AzureAuthService::default() @@ -730,6 +761,9 @@ mod tests { .await .unwrap_err(); - assert!(matches!(error, Error::EmptyAzureToken)); + assert_eq!( + error, + Error::EmptyCallerCredential("Azure AD token provider returned an empty token") + ); } } diff --git a/litellm-rust/crates/auth-azure/src/types.rs b/litellm-rust/crates/auth-azure/src/types.rs index a3a898f000f..a042937a047 100644 --- a/litellm-rust/crates/auth-azure/src/types.rs +++ b/litellm-rust/crates/auth-azure/src/types.rs @@ -117,7 +117,12 @@ fn string_config( None => Ok(ConfigValue::Absent), Some(Value::Null) => Ok(ConfigValue::ExplicitNone(source)), Some(Value::String(value)) => Ok(ConfigValue::Value(Sourced::new(value.clone(), source))), - Some(_) => Err(Error::InvalidFieldType(name.to_string())), + Some(_) => Err(Error::InvalidConfiguration( + litellm_auth_types::ErrorDetail::InvalidType { + field: name.into(), + expected: "a string or null", + }, + )), } } diff --git a/litellm-rust/crates/auth-gcp/Cargo.toml b/litellm-rust/crates/auth-gcp/Cargo.toml index 0c6258a193c..8a3598234e1 100644 --- a/litellm-rust/crates/auth-gcp/Cargo.toml +++ b/litellm-rust/crates/auth-gcp/Cargo.toml @@ -19,3 +19,6 @@ tokio.workspace = true gcp_auth = "0.12.7" google-cloud-auth = { workspace = true, optional = true } http = { workspace = true, optional = true } + +[dev-dependencies] +rstest.workspace = true diff --git a/litellm-rust/crates/auth-gcp/src/lib.rs b/litellm-rust/crates/auth-gcp/src/lib.rs index 4374dff95aa..97bc2c482c3 100644 --- a/litellm-rust/crates/auth-gcp/src/lib.rs +++ b/litellm-rust/crates/auth-gcp/src/lib.rs @@ -299,7 +299,7 @@ fn validate_request_credentials(configured: &str) -> Result<&str, Error> { .map(str::to_string) }); if token_uri.as_deref() != Some(GOOGLE_OAUTH_TOKEN_ENDPOINT) { - return Err(Error::RequestVertexTokenEndpoint); + return Err(Error::InvalidConfiguration("request-controlled Vertex credentials must use the canonical Google OAuth token endpoint".into())); } Ok(configured) } @@ -376,10 +376,20 @@ fn optional_credentials( .map(SecretValue::new) .map(|value| Sourced::new(value, source)) .map(Some) - .map_err(|error| Error::InvalidFieldType(format!("{}: {error}", names[0]))); + .map_err(|error| { + Error::InvalidConfiguration(litellm_auth_types::ErrorDetail::failed( + "credential serialization", + error, + )) + }); } Some(_) => { - return Err(Error::InvalidFieldType(names[0].to_string())); + return Err(Error::InvalidConfiguration( + litellm_auth_types::ErrorDetail::InvalidType { + field: names[0].into(), + expected: "a string or null", + }, + )); } } } @@ -397,7 +407,12 @@ fn optional_string(params: &Map, names: &[&str]) -> Result