mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-12 03:48:56 +00:00
feat(rust): support Tencent Messages API
This commit is contained in:
parent
462962c094
commit
5f2d8adaa3
10 changed files with 528 additions and 0 deletions
|
|
@ -329,6 +329,13 @@
|
|||
"litellm-rust/crates/llms/src/reducto/ocr/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "litellm-rust/crates/llms/src/tencent/messages/AGENTS.md",
|
||||
"description": "Conventions for code under litellm-rust/crates/llms/src/tencent/messages/",
|
||||
"scope": [
|
||||
"litellm-rust/crates/llms/src/tencent/messages/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "litellm-rust/crates/llms/src/vertex_ai/messages/AGENTS.md",
|
||||
"description": "Conventions for code under litellm-rust/crates/llms/src/vertex_ai/messages/",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ pub enum LlmProviders {
|
|||
OpenaiLike,
|
||||
#[strum(serialize = "reducto")]
|
||||
Reducto,
|
||||
#[strum(serialize = "tencent")]
|
||||
Tencent,
|
||||
#[strum(serialize = "vertex_ai")]
|
||||
VertexAi,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use litellm_core_utils::get_llm_provider_logic::LlmProviders;
|
||||
use litellm_http::request::string_headers as shared_string_headers;
|
||||
pub(super) use litellm_http::request::truncate_error_body;
|
||||
use litellm_llms::tencent::messages::transformation::TENCENT_MESSAGES_CONFIG;
|
||||
use litellm_llms::{
|
||||
anthropic::messages::transformation::ANTHROPIC_MESSAGES_CONFIG,
|
||||
azure_ai::messages::transformation::AZURE_ANTHROPIC_MESSAGES_CONFIG,
|
||||
|
|
@ -36,6 +37,7 @@ impl MessagesProvider {
|
|||
/// partner models on this route.
|
||||
pub(crate) fn messages_provider(provider: LlmProviders, model: &str) -> Option<MessagesProvider> {
|
||||
let config: &'static dyn BaseMessagesConfig = match provider {
|
||||
LlmProviders::Tencent => &TENCENT_MESSAGES_CONFIG,
|
||||
LlmProviders::Anthropic => &ANTHROPIC_MESSAGES_CONFIG,
|
||||
LlmProviders::AzureAi => &AZURE_ANTHROPIC_MESSAGES_CONFIG,
|
||||
LlmProviders::Bedrock => &BEDROCK_ANTHROPIC_MESSAGES_CONFIG,
|
||||
|
|
@ -66,6 +68,7 @@ mod tests {
|
|||
use litellm_core_utils::get_llm_provider_logic::LlmProviders;
|
||||
|
||||
#[rstest]
|
||||
#[case::tencent("tencent")]
|
||||
#[case::anthropic("anthropic")]
|
||||
#[case::azure_ai("azure_ai")]
|
||||
#[case::bedrock("bedrock")]
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ mod request;
|
|||
mod response;
|
||||
mod secrets;
|
||||
mod stream;
|
||||
mod tencent;
|
||||
|
||||
const MODEL: &str = "claude-sonnet-4-5";
|
||||
|
||||
|
|
|
|||
260
litellm-rust/crates/inference-messages/tests/messages/tencent.rs
Normal file
260
litellm-rust/crates/inference-messages/tests/messages/tencent.rs
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
use std::ops::ControlFlow;
|
||||
|
||||
use bytes::Bytes;
|
||||
use litellm_llms::base_llm::messages::context::MessagesModelCapabilities;
|
||||
use litellm_llms_types::providers::anthropic::DEFAULT_HEADERS;
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[fixture]
|
||||
fn tencent_call(call: MessagesCall) -> MessagesCall {
|
||||
MessagesCall {
|
||||
custom_llm_provider: Some("tencent".into()),
|
||||
api_key: Some("deployment-key".into()),
|
||||
..call
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::deployment_key(&[], "x-api-key", "deployment-key")]
|
||||
#[case::forwarded_key(&[("X-Api-Key", "caller-key")], "x-api-key", "caller-key")]
|
||||
#[case::forwarded_bearer(&[("Authorization", "Bearer caller")], "authorization", "Bearer caller")]
|
||||
#[tokio::test]
|
||||
async fn credentials_reach_the_selected_provider(
|
||||
tencent_call: MessagesCall,
|
||||
#[case] forwarded: &[(&str, &str)],
|
||||
#[case] header: &str,
|
||||
#[case] credential: &str,
|
||||
) {
|
||||
let upstream = upstream([message_response()]).await;
|
||||
run_message(MessagesCall {
|
||||
api_base: Some(upstream.uri()),
|
||||
extra_headers: headers(forwarded.iter().copied()),
|
||||
..tencent_call
|
||||
})
|
||||
.await;
|
||||
let sent = only_request(&upstream).await;
|
||||
assert_eq!(sent.header_values(header), [credential]);
|
||||
let version = DEFAULT_HEADERS
|
||||
.iter()
|
||||
.find_map(|(name, value)| (*name == "anthropic-version").then_some(*value))
|
||||
.unwrap();
|
||||
assert_eq!(sent.header_values("anthropic-version"), [version]);
|
||||
assert_eq!(sent.json()["model"], MODEL);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn credentials_and_base_resolve_from_the_secret_source(tencent_call: MessagesCall) {
|
||||
let upstream = upstream([message_response()]).await;
|
||||
let base = format!("{}/v1/chat/completions/", upstream.uri());
|
||||
let secrets = Arc::new(RecordingSecrets::new([
|
||||
("TENCENT_API_KEY", "secret-key"),
|
||||
("TENCENT_ANTHROPIC_API_BASE", base.as_str()),
|
||||
("TENCENT_API_BASE", UNREACHABLE_BASE),
|
||||
]));
|
||||
run_with(
|
||||
secrets,
|
||||
MessagesCall {
|
||||
api_key: None,
|
||||
..tencent_call
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let sent = only_request(&upstream).await;
|
||||
assert_eq!(sent.url.path(), "/v1/messages");
|
||||
assert_eq!(sent.header("x-api-key"), Some("secret-key"));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn missing_credentials_fail_before_sending(tencent_call: MessagesCall) {
|
||||
let upstream = upstream([message_response()]).await;
|
||||
let error = run(MessagesCall {
|
||||
api_key: None,
|
||||
api_base: Some(upstream.uri()),
|
||||
..tencent_call
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
error,
|
||||
Error::Auth(litellm_auth::Error::MissingApiKey {
|
||||
environment_variable: "TENCENT_API_KEY",
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(received(&upstream).await.is_empty());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::missing_max_tokens(json!({"max_tokens": null}))]
|
||||
#[case::invalid_metadata(json!({"metadata": {"user_id": 7}}))]
|
||||
#[case::invalid_reasoning_effort(json!({"reasoning_effort": "unknown"}))]
|
||||
#[tokio::test]
|
||||
async fn malformed_requests_fail_before_sending(tencent_call: MessagesCall, #[case] fields: Value) {
|
||||
let upstream = upstream([message_response()]).await;
|
||||
let error = run(with_fields(
|
||||
MessagesCall {
|
||||
api_base: Some(upstream.uri()),
|
||||
..tencent_call
|
||||
},
|
||||
fields,
|
||||
))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(error.is_request(), "{error:?}");
|
||||
assert!(received(&upstream).await.is_empty());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::legacy(false, json!({"type": "enabled", "budget_tokens": 4095}))]
|
||||
#[case::adaptive(true, json!({"type": "adaptive", "display": "summarized"}))]
|
||||
#[tokio::test]
|
||||
async fn reasoning_effort_becomes_model_appropriate_thinking(
|
||||
tencent_call: MessagesCall,
|
||||
#[case] adaptive: bool,
|
||||
#[case] thinking: Value,
|
||||
) {
|
||||
let upstream = upstream([message_response()]).await;
|
||||
run_message(with_fields(
|
||||
MessagesCall {
|
||||
api_base: Some(upstream.uri()),
|
||||
shaping: MessagesShaping {
|
||||
capabilities: MessagesModelCapabilities {
|
||||
supports_reasoning: true,
|
||||
supports_adaptive_thinking: adaptive,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..tencent_call
|
||||
},
|
||||
json!({"max_tokens": 4096, "reasoning_effort": "high"}),
|
||||
))
|
||||
.await;
|
||||
let sent = only_request(&upstream).await.json();
|
||||
assert_eq!(sent["thinking"], thinking);
|
||||
assert_eq!(sent.get("reasoning_effort"), None);
|
||||
assert_eq!(
|
||||
sent.get("output_config"),
|
||||
adaptive.then(|| json!({"effort": "high"})).as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::encrypted_reasoning(json!({"type": "thinking", "thinking": "private", "signature": "litellm_encrypted_reasoning:private"}))]
|
||||
#[case::orphaned_advisor(json!({"type": "server_tool_use", "name": "advisor", "id": "srvtoolu_advisor", "input": {}}))]
|
||||
#[tokio::test]
|
||||
async fn replayed_history_is_cleaned_without_losing_text(
|
||||
tencent_call: MessagesCall,
|
||||
#[case] block: Value,
|
||||
) {
|
||||
let upstream = upstream([message_response()]).await;
|
||||
run_message(with_fields(
|
||||
MessagesCall {
|
||||
api_base: Some(upstream.uri()),
|
||||
..tencent_call
|
||||
},
|
||||
json!({"messages": [
|
||||
{"role": "assistant", "content": [block, {"type": "text", "text": "keep"}]},
|
||||
{"role": "user", "content": "continue"}
|
||||
]}),
|
||||
))
|
||||
.await;
|
||||
assert_eq!(
|
||||
only_request(&upstream).await.json()["messages"],
|
||||
json!([
|
||||
{"role": "assistant", "content": [{"type": "text", "text": "keep"}]},
|
||||
{"role": "user", "content": "continue"}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::billing_string(json!("x-anthropic-billing-header: private"), None)]
|
||||
#[case::mixed_blocks(json!([
|
||||
{"type": "text", "text": "x-anthropic-billing-header: private"},
|
||||
{"type": "text", "text": "keep"}
|
||||
]), Some(json!([{ "type": "text", "text": "keep" }])))]
|
||||
#[case::ordinary_system(json!("keep"), Some(json!("keep")))]
|
||||
#[tokio::test]
|
||||
async fn billing_is_removed_and_tools_and_cache_hints_are_preserved(
|
||||
tencent_call: MessagesCall,
|
||||
#[case] system: Value,
|
||||
#[case] expected: Option<Value>,
|
||||
) {
|
||||
let upstream = upstream([message_response()]).await;
|
||||
let fields = json!({
|
||||
"system": system,
|
||||
"tools": [{"name": "echo", "input_schema": {"type": "object"}}],
|
||||
"messages": [{"role": "user", "content": [{
|
||||
"type": "text", "text": "hi", "cache_control": {"type": "ephemeral", "ttl": "1h"}
|
||||
}]}]
|
||||
});
|
||||
run_message(with_fields(
|
||||
MessagesCall {
|
||||
api_base: Some(upstream.uri()),
|
||||
..tencent_call
|
||||
},
|
||||
fields.clone(),
|
||||
))
|
||||
.await;
|
||||
let sent = only_request(&upstream).await.json();
|
||||
assert_eq!(sent.get("system"), expected.as_ref());
|
||||
assert_eq!(sent["tools"], fields["tools"]);
|
||||
assert_eq!(sent["messages"], fields["messages"]);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StreamSink(Mutex<Vec<u8>>);
|
||||
|
||||
impl litellm_host_native::in_process::StreamConsumer<Messages> for StreamSink {
|
||||
async fn open_stream(
|
||||
&self,
|
||||
_: litellm_inference_messages::route::MessagesStreamHead,
|
||||
) -> Result<ControlFlow<()>, Error> {
|
||||
Ok(ControlFlow::Continue(()))
|
||||
}
|
||||
|
||||
async fn send_chunk(&self, chunk: Bytes) -> Result<ControlFlow<()>, Error> {
|
||||
self.0.lock().unwrap().extend_from_slice(&chunk);
|
||||
Ok(ControlFlow::Continue(()))
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn streaming_relays_provider_events_unchanged(tencent_call: MessagesCall) {
|
||||
let events = concat!(
|
||||
"event: message_start\ndata: {\"type\":\"message_start\"}\n\n",
|
||||
"event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"text_delta\",\"text\":\"OK\"}}\n\n",
|
||||
"event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
);
|
||||
let upstream =
|
||||
upstream([ResponseTemplate::new(200).set_body_raw(events, "text/event-stream")]).await;
|
||||
let host = LocalMessagesHost::new(with_fields(
|
||||
MessagesCall {
|
||||
api_base: Some(upstream.uri()),
|
||||
..tencent_call
|
||||
},
|
||||
json!({"stream": true}),
|
||||
));
|
||||
let sink = StreamSink::default();
|
||||
let outcome = litellm_host_native::in_process::run_hosted(
|
||||
machine(Arc::new(RecordingSecrets::empty()))(host.request().unwrap()),
|
||||
litellm_host_native::in_process::Host {
|
||||
services: &(),
|
||||
interceptors: &host,
|
||||
stream: &sink,
|
||||
observers: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(outcome, MessagesOutput::StreamEnded));
|
||||
assert_eq!(sink.0.into_inner().unwrap(), events.as_bytes());
|
||||
assert_eq!(only_request(&upstream).await.json()["stream"], true);
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ pub mod mistral;
|
|||
pub mod openai;
|
||||
pub mod openai_like;
|
||||
pub mod reducto;
|
||||
pub mod tencent;
|
||||
pub mod vertex_ai;
|
||||
|
||||
pub use error::{Error, ErrorDetail, ErrorSource};
|
||||
|
|
|
|||
11
litellm-rust/crates/llms/src/tencent/messages/AGENTS.md
Normal file
11
litellm-rust/crates/llms/src/tencent/messages/AGENTS.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# rules
|
||||
|
||||
- Reuse Anthropic Messages shaping, reasoning translation, and history cleanup
|
||||
- Use Tencent credentials and endpoints, and strip billing system blocks
|
||||
- Accept a root, versioned base, chat endpoint, or complete Messages endpoint
|
||||
|
||||
# references
|
||||
|
||||
- Python counterpart `litellm/llms/tencent/messages/transformation.py`
|
||||
- https://www.tencentcloud.com/document/product/1300/80632
|
||||
- https://cloud.tencent.com/document/product/1823/132246
|
||||
1
litellm-rust/crates/llms/src/tencent/messages/mod.rs
Normal file
1
litellm-rust/crates/llms/src/tencent/messages/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod transformation;
|
||||
241
litellm-rust/crates/llms/src/tencent/messages/transformation.rs
Normal file
241
litellm-rust/crates/llms/src/tencent/messages/transformation.rs
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
use litellm_auth::{CredentialPlacement, SecretValue};
|
||||
use litellm_core_utils::settings::resolve_non_empty;
|
||||
use litellm_llms_types::{
|
||||
formats::messages::{MessagesOptionalParams, MessagesRequest},
|
||||
providers::anthropic::{DEFAULT_HEADERS, MESSAGES_PATH},
|
||||
};
|
||||
use litellm_router_types::LitellmParams;
|
||||
|
||||
use crate::{
|
||||
Error,
|
||||
anthropic::{
|
||||
common_utils::{filter_billing_headers_from_system, has_anthropic_credential},
|
||||
messages::{
|
||||
handler::shape_anthropic_messages_request,
|
||||
transformation::{transform_messages_request, update_headers_with_anthropic_beta},
|
||||
},
|
||||
},
|
||||
base_llm::{
|
||||
auth::{AuthScheme, Headers, ValidatedEnvironment},
|
||||
messages::{context::MessagesTransformContext, transformation::BaseMessagesConfig},
|
||||
},
|
||||
};
|
||||
|
||||
const API_KEY_ENV: &str = "TENCENT_API_KEY";
|
||||
const API_BASE_ENV: &str = "TENCENT_API_BASE";
|
||||
const ANTHROPIC_API_BASE_ENV: &str = "TENCENT_ANTHROPIC_API_BASE";
|
||||
const DEFAULT_BASE: &str = "https://tokenhub-intl.tencentcloudmaas.com";
|
||||
|
||||
pub struct TencentAnthropicMessagesConfig;
|
||||
pub const TENCENT_MESSAGES_CONFIG: TencentAnthropicMessagesConfig = TencentAnthropicMessagesConfig;
|
||||
|
||||
impl BaseMessagesConfig for TencentAnthropicMessagesConfig {
|
||||
fn shape_request(
|
||||
&self,
|
||||
request: MessagesRequest,
|
||||
reasoning_auto_summary: bool,
|
||||
) -> Result<MessagesRequest, Error> {
|
||||
shape_anthropic_messages_request(request, reasoning_auto_summary)
|
||||
}
|
||||
|
||||
fn validate_environment(
|
||||
&self,
|
||||
headers: Headers,
|
||||
api_key: Option<&str>,
|
||||
_model: &str,
|
||||
_params: &LitellmParams,
|
||||
env: &dyn Fn(&str) -> Option<String>,
|
||||
) -> Result<ValidatedEnvironment, Error> {
|
||||
if has_anthropic_credential(&headers) {
|
||||
return Ok(ValidatedEnvironment {
|
||||
headers,
|
||||
auth: AuthScheme::Forwarded,
|
||||
});
|
||||
}
|
||||
let key = resolve_non_empty(api_key, env, &[API_KEY_ENV]).ok_or(Error::Auth(
|
||||
litellm_auth::Error::MissingApiKey {
|
||||
provider: "Tencent",
|
||||
environment_variable: API_KEY_ENV,
|
||||
},
|
||||
))?;
|
||||
Ok(ValidatedEnvironment {
|
||||
headers,
|
||||
auth: AuthScheme::Credential {
|
||||
placement: CredentialPlacement::Header("x-api-key"),
|
||||
secret: SecretValue::new(key),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn get_complete_url(
|
||||
&self,
|
||||
api_base: Option<&str>,
|
||||
_model: &str,
|
||||
_params: &LitellmParams,
|
||||
_stream: bool,
|
||||
env: &dyn Fn(&str) -> Option<String>,
|
||||
) -> Result<String, Error> {
|
||||
let base = resolve_non_empty(api_base, env, &[ANTHROPIC_API_BASE_ENV, API_BASE_ENV])
|
||||
.unwrap_or_else(|| DEFAULT_BASE.into());
|
||||
let base = base.trim_end_matches('/');
|
||||
if base.ends_with(MESSAGES_PATH) {
|
||||
return Ok(base.into());
|
||||
}
|
||||
let base = base
|
||||
.strip_suffix("/v1/chat/completions")
|
||||
.or_else(|| base.strip_suffix("/v1"))
|
||||
.unwrap_or(base);
|
||||
Ok(format!("{base}{MESSAGES_PATH}"))
|
||||
}
|
||||
|
||||
fn transform_anthropic_messages_request(
|
||||
&self,
|
||||
request: MessagesRequest,
|
||||
context: &MessagesTransformContext,
|
||||
) -> Result<MessagesRequest, Error> {
|
||||
let request = transform_messages_request(request, context)?;
|
||||
Ok(MessagesRequest {
|
||||
params: MessagesOptionalParams {
|
||||
system: request
|
||||
.params
|
||||
.system
|
||||
.and_then(filter_billing_headers_from_system),
|
||||
..request.params
|
||||
},
|
||||
..request
|
||||
})
|
||||
}
|
||||
|
||||
fn secret_names(&self) -> &'static [&'static str] {
|
||||
&[API_KEY_ENV, ANTHROPIC_API_BASE_ENV, API_BASE_ENV]
|
||||
}
|
||||
|
||||
fn default_headers(&self) -> &'static [(&'static str, &'static str)] {
|
||||
DEFAULT_HEADERS
|
||||
}
|
||||
|
||||
fn request_headers(&self, headers: Headers, request: &MessagesRequest) -> Headers {
|
||||
update_headers_with_anthropic_beta(headers, request)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn no_env(_: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::root("https://proxy.test", "https://proxy.test/v1/messages")]
|
||||
#[case::root_slash("https://proxy.test/", "https://proxy.test/v1/messages")]
|
||||
#[case::full_endpoint("https://proxy.test/v1/messages", "https://proxy.test/v1/messages")]
|
||||
#[case::full_endpoint_slash(
|
||||
"https://proxy.test/v1/messages/",
|
||||
"https://proxy.test/v1/messages"
|
||||
)]
|
||||
#[case::custom_path(
|
||||
"https://proxy.test/anthropic",
|
||||
"https://proxy.test/anthropic/v1/messages"
|
||||
)]
|
||||
#[case::versioned("https://proxy.test/v1", "https://proxy.test/v1/messages")]
|
||||
#[case::chat_endpoint(
|
||||
"https://proxy.test/v1/chat/completions/",
|
||||
"https://proxy.test/v1/messages"
|
||||
)]
|
||||
fn endpoint_is_appended_once(#[case] base: &str, #[case] expected: &str) {
|
||||
assert_eq!(
|
||||
TENCENT_MESSAGES_CONFIG.get_complete_url(
|
||||
Some(base),
|
||||
"test-model",
|
||||
&Default::default(),
|
||||
false,
|
||||
&no_env,
|
||||
),
|
||||
Ok(expected.into())
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::default(None, None, None, &format!("{DEFAULT_BASE}{MESSAGES_PATH}"))]
|
||||
#[case::explicit(
|
||||
Some("https://explicit.test"),
|
||||
Some("https://anthropic.test"),
|
||||
Some("https://env.test"),
|
||||
"https://explicit.test/v1/messages"
|
||||
)]
|
||||
#[case::anthropic_base(
|
||||
None,
|
||||
Some("https://anthropic.test"),
|
||||
Some("https://env.test"),
|
||||
"https://anthropic.test/v1/messages"
|
||||
)]
|
||||
#[case::generic_base(
|
||||
None,
|
||||
None,
|
||||
Some("https://env.test/v1"),
|
||||
"https://env.test/v1/messages"
|
||||
)]
|
||||
#[case::blank_anthropic_base(
|
||||
None,
|
||||
Some(" "),
|
||||
Some("https://env.test"),
|
||||
"https://env.test/v1/messages"
|
||||
)]
|
||||
#[case::blank_explicit(
|
||||
Some(" "),
|
||||
Some("https://anthropic.test/v1/messages/"),
|
||||
None,
|
||||
"https://anthropic.test/v1/messages"
|
||||
)]
|
||||
fn base_precedence(
|
||||
#[case] base: Option<&str>,
|
||||
#[case] anthropic_base: Option<&str>,
|
||||
#[case] generic_base: Option<&str>,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let env = |name: &str| match name {
|
||||
ANTHROPIC_API_BASE_ENV => anthropic_base.map(str::to_owned),
|
||||
API_BASE_ENV => generic_base.map(str::to_owned),
|
||||
_ => None,
|
||||
};
|
||||
assert_eq!(
|
||||
TENCENT_MESSAGES_CONFIG.get_complete_url(
|
||||
base,
|
||||
"test-model",
|
||||
&Default::default(),
|
||||
false,
|
||||
&env,
|
||||
),
|
||||
Ok(expected.into())
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::explicit(Some("explicit"), Some("environment"), "explicit")]
|
||||
#[case::environment(None, Some("environment"), "environment")]
|
||||
#[case::blank_explicit(Some(" "), Some("environment"), "environment")]
|
||||
fn key_precedence(
|
||||
#[case] key: Option<&str>,
|
||||
#[case] env_key: Option<&str>,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let env = |name: &str| {
|
||||
(name == API_KEY_ENV)
|
||||
.then(|| env_key.map(str::to_owned))
|
||||
.flatten()
|
||||
};
|
||||
let environment = TENCENT_MESSAGES_CONFIG
|
||||
.validate_environment(vec![], key, "test-model", &Default::default(), &env)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
environment.auth,
|
||||
AuthScheme::Credential {
|
||||
placement: CredentialPlacement::Header("x-api-key"), ref secret
|
||||
} if secret.expose() == expected
|
||||
));
|
||||
}
|
||||
}
|
||||
1
litellm-rust/crates/llms/src/tencent/mod.rs
Normal file
1
litellm-rust/crates/llms/src/tencent/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod messages;
|
||||
Loading…
Add table
Reference in a new issue