From 403dd14580f6b96b48fd2d433019da5b2d82bcb2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 03:13:05 +0000 Subject: [PATCH] refactor(rust/messages): split Anthropic config into its own provider file and type the request/response contract Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com> --- .../crates/ai-gateway/src/messages/handler.rs | 11 +- .../crates/ai-gateway/src/messages/prepare.rs | 10 +- .../core/src/messages/transformation.rs | 33 +-- .../crates/core/src/messages/types.rs | 110 ++++++++- .../src/providers/anthropic/messages/mod.rs | 1 + .../anthropic/messages/transformation.rs | 142 +++++++++++ .../core/src/providers/anthropic/mod.rs | 1 + .../azure_ai/messages/transformation.rs | 221 ++++++++++-------- litellm-rust/crates/core/src/providers/mod.rs | 1 + 9 files changed, 397 insertions(+), 133 deletions(-) create mode 100644 litellm-rust/crates/core/src/providers/anthropic/messages/mod.rs create mode 100644 litellm-rust/crates/core/src/providers/anthropic/messages/transformation.rs create mode 100644 litellm-rust/crates/core/src/providers/anthropic/mod.rs diff --git a/litellm-rust/crates/ai-gateway/src/messages/handler.rs b/litellm-rust/crates/ai-gateway/src/messages/handler.rs index ad58d74a87f..dd4a2f22aa7 100644 --- a/litellm-rust/crates/ai-gateway/src/messages/handler.rs +++ b/litellm-rust/crates/ai-gateway/src/messages/handler.rs @@ -35,12 +35,13 @@ pub(super) async fn execute_messages_provider_call( }); } - let response_json: Value = serde_json::from_str(&text).map_err(|err| { + let response = serde_json::from_str(&text).map_err(|err| { CoreError::InvalidResponse(format!("invalid messages response JSON: {err}")) })?; - - Ok(request + let transformed = request .config - .transform_response(&request.model, response_json)? - .into_json()) + .transform_response(&request.model, response)?; + serde_json::to_value(transformed).map_err(|err| { + CoreError::InvalidResponse(format!("failed to serialize messages response: {err}")) + }) } diff --git a/litellm-rust/crates/ai-gateway/src/messages/prepare.rs b/litellm-rust/crates/ai-gateway/src/messages/prepare.rs index 72e32197267..47105b39954 100644 --- a/litellm-rust/crates/ai-gateway/src/messages/prepare.rs +++ b/litellm-rust/crates/ai-gateway/src/messages/prepare.rs @@ -51,7 +51,15 @@ pub(super) fn prepare_messages_call( } let url = config.complete_url(request.api_base, &model, &env_lookup)?; - let body = config.transform_request(request.body)?.body; + let typed_request = serde_json::from_value(request.body).map_err(|err| { + CoreError::InvalidRequest(format!("invalid Anthropic messages request: {err}")) + })?; + let transformed = config.transform_request(typed_request)?; + let body = serde_json::to_value(transformed).map_err(|err| { + CoreError::InvalidRequest(format!( + "failed to serialize Anthropic messages request: {err}" + )) + })?; Ok(ProviderMessagesRequest { model, diff --git a/litellm-rust/crates/core/src/messages/transformation.rs b/litellm-rust/crates/core/src/messages/transformation.rs index 448f7dd6dc8..3a34a58de6f 100644 --- a/litellm-rust/crates/core/src/messages/transformation.rs +++ b/litellm-rust/crates/core/src/messages/transformation.rs @@ -1,8 +1,6 @@ -use serde_json::Value; +use crate::error::CoreResult; -use crate::error::{json_type_name, CoreError, CoreResult}; - -use super::types::{MessagesRequestData, MessagesResponseData}; +use super::types::{AnthropicMessagesRequest, AnthropicMessagesResponse}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum MessagesAuthStrategy { @@ -44,29 +42,18 @@ pub trait AnthropicMessagesProviderConfig: Sync { ] } - fn transform_request(&self, body: Value) -> CoreResult { - if !body.is_object() { - return Err(CoreError::InvalidType { - expected: "object", - actual: json_type_name(&body), - }); - } - Ok(MessagesRequestData { body }) + fn transform_request( + &self, + request: AnthropicMessagesRequest, + ) -> CoreResult { + Ok(request) } fn transform_response( &self, _model: &str, - response_json: Value, - ) -> CoreResult { - if !response_json.is_object() { - return Err(CoreError::InvalidType { - expected: "object", - actual: json_type_name(&response_json), - }); - } - Ok(MessagesResponseData { - body: response_json, - }) + response: AnthropicMessagesResponse, + ) -> CoreResult { + Ok(response) } } diff --git a/litellm-rust/crates/core/src/messages/types.rs b/litellm-rust/crates/core/src/messages/types.rs index e56e924e910..11fe17ea40f 100644 --- a/litellm-rust/crates/core/src/messages/types.rs +++ b/litellm-rust/crates/core/src/messages/types.rs @@ -1,18 +1,110 @@ use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{Map, Value}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct MessagesRequestData { - pub body: Value, +#[serde(untagged)] +pub enum SystemPrompt { + Text(String), + Blocks(Vec), } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct MessagesResponseData { - pub body: Value, +#[serde(untagged)] +pub enum MessageContent { + Text(String), + Blocks(Vec), } -impl MessagesResponseData { - pub fn into_json(self) -> Value { - self.body - } +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct ContentBlock { + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_control: Option, + #[serde(flatten)] + pub extra: Map, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct CacheControl { + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub cache_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ttl: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub scope: Option, + #[serde(flatten)] + pub extra: Map, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct AnthropicMessage { + pub role: String, + pub content: MessageContent, + #[serde(flatten)] + pub extra: Map, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct AnthropicMessagesRequest { + pub model: String, + pub messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub system: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub stop_sequences: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub stream: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub top_p: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub top_k: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_choice: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub thinking: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub service_tier: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub container: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mcp_servers: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub context_management: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub output_config: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub speed: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub inference_geo: Option, + #[serde(flatten)] + pub extra: Map, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct AnthropicMessagesResponse { + pub id: String, + #[serde(rename = "type")] + pub message_type: String, + pub role: String, + pub model: String, + pub content: Vec, + // Anthropic always includes stop_reason / stop_sequence, null until the turn + // ends; serialize them even when None so callers see the same shape as Python. + pub stop_reason: Option, + pub stop_sequence: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub usage: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub container: Option, + #[serde(flatten)] + pub extra: Map, } diff --git a/litellm-rust/crates/core/src/providers/anthropic/messages/mod.rs b/litellm-rust/crates/core/src/providers/anthropic/messages/mod.rs new file mode 100644 index 00000000000..f239b6921fa --- /dev/null +++ b/litellm-rust/crates/core/src/providers/anthropic/messages/mod.rs @@ -0,0 +1 @@ +pub mod transformation; diff --git a/litellm-rust/crates/core/src/providers/anthropic/messages/transformation.rs b/litellm-rust/crates/core/src/providers/anthropic/messages/transformation.rs new file mode 100644 index 00000000000..829f2260d3c --- /dev/null +++ b/litellm-rust/crates/core/src/providers/anthropic/messages/transformation.rs @@ -0,0 +1,142 @@ +use crate::error::{CoreError, CoreResult}; +use crate::messages::transformation::{AnthropicMessagesProviderConfig, MessagesAuthStrategy}; + +const ANTHROPIC_API_KEY_ENV: &str = "ANTHROPIC_API_KEY"; +const ANTHROPIC_API_BASE_ENV: &str = "ANTHROPIC_API_BASE"; +const DEFAULT_ANTHROPIC_API_BASE: &str = "https://api.anthropic.com"; +const MESSAGES_PATH_SUFFIX: &str = "/v1/messages"; + +pub struct AnthropicMessagesConfig; + +pub const ANTHROPIC_MESSAGES_CONFIG: AnthropicMessagesConfig = AnthropicMessagesConfig; + +pub fn non_empty(value: Option<&str>) -> Option<&str> { + value.map(str::trim).filter(|value| !value.is_empty()) +} + +pub fn resolve_anthropic_api_key( + api_key: Option<&str>, + env_lookup: &dyn Fn(&str) -> Option, +) -> CoreResult { + non_empty(api_key) + .map(str::to_string) + .or_else(|| env_lookup(ANTHROPIC_API_KEY_ENV).filter(|value| !value.trim().is_empty())) + .ok_or_else(|| { + CoreError::Auth( + "Missing Anthropic API Key - Set `api_key` or the ANTHROPIC_API_KEY \ + environment variable" + .to_string(), + ) + }) +} + +pub fn complete_anthropic_url( + api_base: Option<&str>, + env_lookup: &dyn Fn(&str) -> Option, +) -> String { + let api_base = non_empty(api_base) + .map(str::to_string) + .or_else(|| env_lookup(ANTHROPIC_API_BASE_ENV).filter(|value| !value.trim().is_empty())) + .unwrap_or_else(|| DEFAULT_ANTHROPIC_API_BASE.to_string()); + + let api_base = api_base.trim_end_matches('/'); + if api_base.ends_with(MESSAGES_PATH_SUFFIX) { + return api_base.to_string(); + } + format!("{api_base}{MESSAGES_PATH_SUFFIX}") +} + +impl AnthropicMessagesProviderConfig for AnthropicMessagesConfig { + fn complete_url( + &self, + api_base: Option<&str>, + _model: &str, + env_lookup: &dyn Fn(&str) -> Option, + ) -> CoreResult { + Ok(complete_anthropic_url(api_base, env_lookup)) + } + + fn resolve_api_key( + &self, + api_key: Option<&str>, + env_lookup: &dyn Fn(&str) -> Option, + ) -> CoreResult { + resolve_anthropic_api_key(api_key, env_lookup) + } + + fn auth_strategy(&self) -> MessagesAuthStrategy { + MessagesAuthStrategy::Header("x-api-key") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn url_defaults_to_public_anthropic_endpoint() { + assert_eq!( + complete_anthropic_url(None, &|_| None), + "https://api.anthropic.com/v1/messages" + ); + } + + #[test] + fn url_appends_messages_suffix_to_custom_base() { + assert_eq!( + complete_anthropic_url(Some("https://proxy.internal"), &|_| None), + "https://proxy.internal/v1/messages" + ); + } + + #[test] + fn url_leaves_complete_messages_endpoint_untouched() { + assert_eq!( + complete_anthropic_url(Some("https://proxy.internal/v1/messages"), &|_| None), + "https://proxy.internal/v1/messages" + ); + } + + #[test] + fn url_falls_back_to_env_base() { + let with_env = |key: &str| { + (key == ANTHROPIC_API_BASE_ENV).then(|| "https://env.anthropic".to_string()) + }; + assert_eq!( + complete_anthropic_url(Some(" "), &with_env), + "https://env.anthropic/v1/messages" + ); + } + + #[test] + fn api_key_prefers_param_then_env_then_errors() { + assert_eq!( + resolve_anthropic_api_key(Some("sk-param"), &|_| None).unwrap(), + "sk-param" + ); + let with_env = |key: &str| (key == ANTHROPIC_API_KEY_ENV).then(|| "sk-env".to_string()); + assert_eq!( + resolve_anthropic_api_key(Some(" "), &with_env).unwrap(), + "sk-env" + ); + assert!(matches!( + resolve_anthropic_api_key(None, &|_| None).expect_err("missing key"), + CoreError::Auth(_) + )); + } + + #[test] + fn auth_strategy_and_default_headers_match_anthropic() { + assert_eq!( + ANTHROPIC_MESSAGES_CONFIG.auth_strategy().header_name(), + "x-api-key" + ); + assert_eq!( + ANTHROPIC_MESSAGES_CONFIG.default_headers(), + &[ + ("anthropic-version", "2023-06-01"), + ("content-type", "application/json"), + ] + ); + } +} diff --git a/litellm-rust/crates/core/src/providers/anthropic/mod.rs b/litellm-rust/crates/core/src/providers/anthropic/mod.rs new file mode 100644 index 00000000000..ba63992f3cb --- /dev/null +++ b/litellm-rust/crates/core/src/providers/anthropic/mod.rs @@ -0,0 +1 @@ +pub mod messages; diff --git a/litellm-rust/crates/core/src/providers/azure_ai/messages/transformation.rs b/litellm-rust/crates/core/src/providers/azure_ai/messages/transformation.rs index 4ac1e521d45..fd0fa3b60ee 100644 --- a/litellm-rust/crates/core/src/providers/azure_ai/messages/transformation.rs +++ b/litellm-rust/crates/core/src/providers/azure_ai/messages/transformation.rs @@ -1,20 +1,26 @@ -use serde_json::{Map, Value}; - -use crate::error::{json_type_name, CoreError, CoreResult}; +use crate::error::{CoreError, CoreResult}; use crate::messages::transformation::{AnthropicMessagesProviderConfig, MessagesAuthStrategy}; -use crate::messages::types::MessagesRequestData; +use crate::messages::types::{ + AnthropicMessage, AnthropicMessagesRequest, AnthropicMessagesResponse, ContentBlock, + MessageContent, SystemPrompt, +}; +use crate::providers::anthropic::messages::transformation::{ + non_empty, AnthropicMessagesConfig, ANTHROPIC_MESSAGES_CONFIG, +}; const AZURE_API_KEY_ENV: &str = "AZURE_API_KEY"; const AZURE_API_BASE_ENV: &str = "AZURE_API_BASE"; +const ANTHROPIC_PATH_SEGMENT: &str = "/anthropic"; +const MESSAGES_PATH_SUFFIX: &str = "/v1/messages"; -pub struct AzureAnthropicMessagesConfig; +pub struct AzureAnthropicMessagesConfig { + anthropic: AnthropicMessagesConfig, +} pub const AZURE_ANTHROPIC_MESSAGES_CONFIG: AzureAnthropicMessagesConfig = - AzureAnthropicMessagesConfig; - -fn non_empty(value: Option<&str>) -> Option<&str> { - value.map(str::trim).filter(|value| !value.is_empty()) -} + AzureAnthropicMessagesConfig { + anthropic: ANTHROPIC_MESSAGES_CONFIG, + }; pub fn resolve_azure_api_key( api_key: Option<&str>, @@ -48,42 +54,32 @@ pub fn complete_azure_anthropic_url( let api_base = api_base.trim_end_matches('/'); - if api_base.ends_with("/v1/messages") || api_base.ends_with("/anthropic/v1/messages") { + if api_base.ends_with(MESSAGES_PATH_SUFFIX) { return Ok(api_base.to_string()); } - let with_anthropic = match api_base.split_once("/anthropic") { - Some((prefix, _)) => format!("{prefix}/anthropic"), - None => format!("{api_base}/anthropic"), + let with_anthropic = match api_base.split_once(ANTHROPIC_PATH_SEGMENT) { + Some((prefix, _)) => format!("{prefix}{ANTHROPIC_PATH_SEGMENT}"), + None => format!("{api_base}{ANTHROPIC_PATH_SEGMENT}"), }; - Ok(format!("{with_anthropic}/v1/messages")) + Ok(format!("{with_anthropic}{MESSAGES_PATH_SUFFIX}")) } -fn remove_scope_from_content_blocks(content: &mut [Value]) { - for item in content.iter_mut() { - if let Some(cache_control) = item - .as_object_mut() - .and_then(|block| block.get_mut("cache_control")) - .and_then(Value::as_object_mut) - { - cache_control.remove("scope"); - } +fn strip_scope_from_block(block: &mut ContentBlock) { + if let Some(cache_control) = block.cache_control.as_mut() { + cache_control.scope = None; } } -fn remove_scope_from_cache_control(body: &mut Map) { - if let Some(Value::Array(system)) = body.get_mut("system") { - remove_scope_from_content_blocks(system); +fn strip_scope_from_system(system: &mut SystemPrompt) { + if let SystemPrompt::Blocks(blocks) = system { + blocks.iter_mut().for_each(strip_scope_from_block); } - if let Some(Value::Array(messages)) = body.get_mut("messages") { - for message in messages.iter_mut() { - if let Some(Value::Array(content)) = message - .as_object_mut() - .and_then(|message| message.get_mut("content")) - { - remove_scope_from_content_blocks(content); - } - } +} + +fn strip_scope_from_message(message: &mut AnthropicMessage) { + if let MessageContent::Blocks(blocks) = &mut message.content { + blocks.iter_mut().for_each(strip_scope_from_block); } } @@ -106,23 +102,33 @@ impl AnthropicMessagesProviderConfig for AzureAnthropicMessagesConfig { } fn auth_strategy(&self) -> MessagesAuthStrategy { - MessagesAuthStrategy::Header("x-api-key") + self.anthropic.auth_strategy() } - fn transform_request(&self, body: Value) -> CoreResult { - let mut body = match body { - Value::Object(body) => body, - other => { - return Err(CoreError::InvalidType { - expected: "object", - actual: json_type_name(&other), - }) - } - }; - remove_scope_from_cache_control(&mut body); - Ok(MessagesRequestData { - body: Value::Object(body), - }) + fn default_headers(&self) -> &'static [(&'static str, &'static str)] { + self.anthropic.default_headers() + } + + fn transform_request( + &self, + mut request: AnthropicMessagesRequest, + ) -> CoreResult { + if let Some(system) = request.system.as_mut() { + strip_scope_from_system(system); + } + request + .messages + .iter_mut() + .for_each(strip_scope_from_message); + self.anthropic.transform_request(request) + } + + fn transform_response( + &self, + model: &str, + response: AnthropicMessagesResponse, + ) -> CoreResult { + self.anthropic.transform_response(model, response) } } @@ -131,6 +137,14 @@ mod tests { use super::*; use serde_json::json; + fn request_from(value: serde_json::Value) -> AnthropicMessagesRequest { + serde_json::from_value(value).expect("valid request") + } + + fn to_value(request: AnthropicMessagesRequest) -> serde_json::Value { + serde_json::to_value(request).expect("serializable request") + } + #[test] fn url_appends_anthropic_and_messages_suffix() { let url = @@ -234,7 +248,7 @@ mod tests { #[test] fn transform_request_strips_scope_from_system_and_messages() { - let body = json!({ + let request = request_from(json!({ "model": "claude-sonnet-4-5", "max_tokens": 1024, "system": [ @@ -257,12 +271,13 @@ mod tests { ] } ] - }); + })); - let transformed = AZURE_ANTHROPIC_MESSAGES_CONFIG - .transform_request(body) - .expect("request transforms") - .body; + let transformed = to_value( + AZURE_ANTHROPIC_MESSAGES_CONFIG + .transform_request(request) + .expect("request transforms"), + ); assert_eq!( transformed["system"][0]["cache_control"], @@ -280,66 +295,82 @@ mod tests { #[test] fn transform_request_is_idempotent_and_preserves_string_system() { - let body = json!({ + let request = request_from(json!({ "model": "claude-sonnet-4-5", + "max_tokens": 16, "system": "plain string system", "messages": [{"role": "user", "content": "hi"}] - }); + })); let once = AZURE_ANTHROPIC_MESSAGES_CONFIG - .transform_request(body) - .expect("request transforms") - .body; + .transform_request(request) + .expect("request transforms"); let twice = AZURE_ANTHROPIC_MESSAGES_CONFIG .transform_request(once.clone()) - .expect("request transforms") - .body; + .expect("request transforms"); assert_eq!(once, twice); - assert_eq!(once["system"], json!("plain string system")); + assert_eq!(to_value(once)["system"], json!("plain string system")); + } + + #[test] + fn transform_request_preserves_all_supported_params() { + let body = json!({ + "model": "claude-sonnet-4-5", + "max_tokens": 256, + "messages": [{"role": "user", "content": "hi"}], + "system": "be terse", + "metadata": {"user_id": "u1"}, + "stop_sequences": ["STOP"], + "stream": false, + "temperature": 0.4, + "top_p": 0.9, + "top_k": 40, + "tools": [{"name": "get_weather", "input_schema": {"type": "object"}}], + "tool_choice": {"type": "auto"}, + "thinking": {"type": "enabled", "budget_tokens": 1024}, + "service_tier": "auto", + "container": {"id": "c1"}, + "mcp_servers": [{"type": "url", "url": "https://mcp.example", "name": "x"}], + "context_management": {"edits": []}, + "output_format": {"type": "json_schema"}, + "output_config": {"effort": "high"}, + "speed": "fast", + "inference_geo": "us", + "litellm_metadata": {"trace": "abc"} + }); + let transformed = to_value( + AZURE_ANTHROPIC_MESSAGES_CONFIG + .transform_request(request_from(body.clone())) + .expect("request transforms"), + ); + assert_eq!(transformed, body); } #[test] fn transform_request_rejects_non_object_body() { - let err = AZURE_ANTHROPIC_MESSAGES_CONFIG - .transform_request(json!("bad")) + let err = serde_json::from_value::(json!("bad")) .expect_err("non-object body should error"); - assert_eq!( - err, - CoreError::InvalidType { - expected: "object", - actual: "string", - } - ); + assert!(err.is_data()); } #[test] - fn transform_response_passes_through_object() { - let response = json!({ + fn transform_response_passes_through() { + let response: AnthropicMessagesResponse = serde_json::from_value(json!({ "id": "msg_1", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "hello"}], "model": "claude-sonnet-4-5", "stop_reason": "end_turn", + "stop_sequence": null, "usage": {"input_tokens": 1, "output_tokens": 2} - }); + })) + .expect("valid response"); let transformed = AZURE_ANTHROPIC_MESSAGES_CONFIG - .transform_response("claude-sonnet-4-5", response.clone()) - .expect("response transforms") - .into_json(); - assert_eq!(transformed, response); - } - - #[test] - fn transform_response_rejects_non_object() { - let err = AZURE_ANTHROPIC_MESSAGES_CONFIG - .transform_response("claude-sonnet-4-5", json!([1, 2, 3])) - .expect_err("array response should error"); - assert_eq!( - err, - CoreError::InvalidType { - expected: "object", - actual: "array", - } - ); + .transform_response("claude-sonnet-4-5", response) + .expect("response transforms"); + let value = serde_json::to_value(transformed).expect("serializable"); + assert_eq!(value["stop_reason"], json!("end_turn")); + assert_eq!(value["stop_sequence"], json!(null)); + assert_eq!(value["content"][0]["text"], json!("hello")); } } diff --git a/litellm-rust/crates/core/src/providers/mod.rs b/litellm-rust/crates/core/src/providers/mod.rs index d75e750a0ba..dc9dc515e7d 100644 --- a/litellm-rust/crates/core/src/providers/mod.rs +++ b/litellm-rust/crates/core/src/providers/mod.rs @@ -1,3 +1,4 @@ +pub mod anthropic; pub mod azure_ai; pub mod mistral; pub mod openai;