mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
Merge remote-tracking branch 'origin/main' into litellm_mcp_listed_tool_metadata
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> # Conflicts: # litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
This commit is contained in:
commit
0ae683b45b
356 changed files with 18502 additions and 2951 deletions
|
|
@ -89,6 +89,7 @@ legacy_paths() {
|
|||
proxy-db-auth-checks)
|
||||
echo tests/unit/proxy/auth/test_auth_checks.py
|
||||
echo tests/unit/proxy/auth/test_user_api_key_auth.py
|
||||
echo tests/unit/proxy/test_credential_slot_registry.py
|
||||
echo tests/unit/proxy/test_deprecated_key_grace_period.py ;;
|
||||
proxy-db-budgets)
|
||||
echo tests/unit/proxy/auth/test_default_end_user_budget_simple.py
|
||||
|
|
|
|||
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
|
|
@ -1,10 +1,2 @@
|
|||
/ui/ @yuneng-berri @ryan-crabbe-berri
|
||||
/litellm/proxy/_experimental/out/ @yuneng-berri @ryan-crabbe-berri
|
||||
/ui/Dockerfile
|
||||
/ui/nginx.conf
|
||||
/ui/litellm-dashboard/src/lib/http/schema.d.ts
|
||||
/ui/litellm-dashboard/tsconfig.tsbuildinfo
|
||||
/model_prices_and_context_window.json @mateo-berri @ryan-crabbe-berri @kerry-berri
|
||||
/litellm/model_prices_and_context_window_backup.json @mateo-berri @ryan-crabbe-berri @kerry-berri
|
||||
/litellm-proxy-extras/litellm_proxy_extras/migrations/ @yuneng-berri @ryan-crabbe-berri
|
||||
/.github/CODEOWNERS @yuneng-berri
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ model_list:
|
|||
- model_name: sagemaker-completion-model
|
||||
litellm_params:
|
||||
model: sagemaker/berri-benchmarking-Llama-2-70b-chat-hf-4
|
||||
input_cost_per_second: 0.000420
|
||||
cost_per_second: 0.000420
|
||||
- model_name: text-embedding-ada-002
|
||||
litellm_params:
|
||||
model: azure/azure-embedding-model
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
CREATE TABLE IF NOT EXISTS "LiteLLM_DailyModelUsage" (
|
||||
"date" TEXT NOT NULL,
|
||||
"model_group" TEXT NOT NULL,
|
||||
"model" TEXT NOT NULL,
|
||||
"custom_llm_provider" TEXT NOT NULL,
|
||||
"task_type" TEXT NOT NULL,
|
||||
"spend" DOUBLE PRECISION NOT NULL DEFAULT 0.0,
|
||||
"prompt_tokens" BIGINT NOT NULL DEFAULT 0,
|
||||
"completion_tokens" BIGINT NOT NULL DEFAULT 0,
|
||||
"request_count" BIGINT NOT NULL DEFAULT 0,
|
||||
"successful_requests" BIGINT NOT NULL DEFAULT 0,
|
||||
"failed_requests" BIGINT NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "LiteLLM_DailyModelUsage_pkey" PRIMARY KEY ("date", "model_group", "model", "custom_llm_provider", "task_type")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "LiteLLM_DailyModelUsage_date_idx" ON "LiteLLM_DailyModelUsage"("date");
|
||||
CREATE INDEX IF NOT EXISTS "LiteLLM_DailyModelUsage_model_group_idx" ON "LiteLLM_DailyModelUsage"("model_group");
|
||||
|
|
@ -1260,6 +1260,26 @@ model LiteLLM_DailyToolSpend {
|
|||
@@id([date, tool_name])
|
||||
}
|
||||
|
||||
model LiteLLM_DailyModelUsage {
|
||||
date String
|
||||
model_group String
|
||||
model String
|
||||
custom_llm_provider String
|
||||
task_type String
|
||||
spend Float @default(0.0)
|
||||
prompt_tokens BigInt @default(0)
|
||||
completion_tokens BigInt @default(0)
|
||||
request_count BigInt @default(0)
|
||||
successful_requests BigInt @default(0)
|
||||
failed_requests BigInt @default(0)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
@@id([date, model_group, model, custom_llm_provider, task_type])
|
||||
@@index([date])
|
||||
@@index([model_group])
|
||||
}
|
||||
|
||||
// Gateway request counts recorded at the ASGI edge by
|
||||
// BillableRequestMetricsMiddleware. This is the source of truth for SGR
|
||||
// (successful gateway requests): it counts what the proxy actually answered,
|
||||
|
|
|
|||
38
litellm-rust/Cargo.lock
generated
38
litellm-rust/Cargo.lock
generated
|
|
@ -3619,7 +3619,6 @@ dependencies = [
|
|||
"litellm-auth",
|
||||
"litellm-host",
|
||||
"litellm-host-python",
|
||||
"litellm-types",
|
||||
"proptest",
|
||||
"pyo3",
|
||||
"rstest",
|
||||
|
|
@ -3658,9 +3657,9 @@ dependencies = [
|
|||
"litellm-host-native",
|
||||
"litellm-http",
|
||||
"litellm-llms",
|
||||
"litellm-llms-types",
|
||||
"litellm-secrets",
|
||||
"litellm-tracing",
|
||||
"litellm-types",
|
||||
"mime_guess",
|
||||
"moka",
|
||||
"rand 0.8.7",
|
||||
|
|
@ -3688,13 +3687,12 @@ name = "litellm-core-utils"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"fancy-regex 0.19.2",
|
||||
"litellm-llms-types",
|
||||
"litellm-tracing",
|
||||
"litellm-types",
|
||||
"rstest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_with",
|
||||
"strum",
|
||||
"thiserror 2.0.19",
|
||||
"url",
|
||||
|
|
@ -3824,9 +3822,9 @@ dependencies = [
|
|||
"litellm-host-http",
|
||||
"litellm-http",
|
||||
"litellm-llms",
|
||||
"litellm-llms-types",
|
||||
"litellm-router",
|
||||
"litellm-secrets",
|
||||
"litellm-types",
|
||||
"rstest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -3999,9 +3997,9 @@ dependencies = [
|
|||
"litellm-framing",
|
||||
"litellm-host",
|
||||
"litellm-http",
|
||||
"litellm-llms-types",
|
||||
"litellm-python-compat",
|
||||
"litellm-secrets",
|
||||
"litellm-types",
|
||||
"reqwest 0.12.28",
|
||||
"rstest",
|
||||
"serde",
|
||||
|
|
@ -4015,13 +4013,26 @@ dependencies = [
|
|||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litellm-llms-types"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"macro_rules_attribute",
|
||||
"rstest",
|
||||
"schemars 1.2.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"strum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litellm-model-catalog"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"jsonschema",
|
||||
"litellm-types",
|
||||
"litellm-llms-types",
|
||||
"rstest",
|
||||
"schemars 1.2.2",
|
||||
"serde",
|
||||
|
|
@ -4059,12 +4070,12 @@ dependencies = [
|
|||
"litellm-host-python",
|
||||
"litellm-http",
|
||||
"litellm-llms",
|
||||
"litellm-llms-types",
|
||||
"litellm-secrets",
|
||||
"litellm-secrets-aws",
|
||||
"litellm-secrets-types",
|
||||
"litellm-token-counter",
|
||||
"litellm-tracing",
|
||||
"litellm-types",
|
||||
"pyo3",
|
||||
"pyo3-async-runtimes",
|
||||
"qdrant-client",
|
||||
|
|
@ -4354,17 +4365,6 @@ dependencies = [
|
|||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litellm-types"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rstest",
|
||||
"schemars 1.2.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.2"
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ litellm-secrets-azure = { path = "crates/secrets-azure" }
|
|||
litellm-secrets-cyberark = { path = "crates/secrets-cyberark" }
|
||||
litellm-http = { path = "crates/http" }
|
||||
litellm-llms = { path = "crates/llms" }
|
||||
litellm-types = { path = "crates/types" }
|
||||
litellm-llms-types = { path = "crates/llms-types" }
|
||||
litellm-core-utils = { path = "crates/core-utils" }
|
||||
litellm-db = { path = "crates/db" }
|
||||
litellm-db-testing = { path = "crates/db-testing" }
|
||||
|
|
@ -74,6 +74,7 @@ proptest = "1.7.0"
|
|||
pyo3 = "0.29.2"
|
||||
pyo3-async-runtimes = { version = "0.29.0", features = ["tokio-runtime"] }
|
||||
rand = "0.8"
|
||||
macro_rules_attribute = "0.2.3"
|
||||
schemars = "1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls", "http2", "stream"] }
|
||||
qdrant-client = { version = "1.19.0", default-features = false }
|
||||
|
|
|
|||
|
|
@ -24,6 +24,6 @@ Keep unary caching independent of stream-only methods. Store streams only after
|
|||
|
||||
Test each contract in its owner: storage capabilities in backend tests, envelopes and freshness here, reuse and replay in core, Python callback and fallback behavior at the bridge, and HTTP behavior at the gateway. Run backend contract checks and Python response-codec fixtures before exposing a new backend
|
||||
|
||||
`ScopedCache` requires an explicit shared or isolated scope at construction. `CacheOptions` has no default sharing policy. Callers may override policy per invocation without replacing the attached service. Versioned native envelopes reject incompatible API surfaces and versions as misses; this envelope is distinct from the legacy Python response codec
|
||||
`ScopedCache` requires an explicit shared or isolated scope at construction. Per-call `CachePolicy` controls reads, writes, expiry, and freshness without replacing the attached scope or service. `CacheOptions` binds that policy to an explicit scope for storage requests and has no default sharing policy. Versioned native envelopes reject incompatible API surfaces and versions as misses; this envelope is distinct from the legacy Python response codec
|
||||
|
||||
Response storage is not the source of budget or rate-limit coordination dependencies. Keep counters, reservations, and atomic admission operations out of `ResponseCacheService`, including when both services happen to use Redis
|
||||
|
|
|
|||
|
|
@ -17,6 +17,6 @@ pub use exact::{ConnectionProbe, ExactResponseCache};
|
|||
pub use response::{ResponseCache, ResponseCacheRequest};
|
||||
|
||||
pub use service::{
|
||||
CacheOptions, CacheScope, ResponseCacheConfig, ResponseCacheService, ResponseEnvelope,
|
||||
ScopedCache,
|
||||
CacheOptions, CachePolicy, CacheScope, ResponseCacheConfig, ResponseCacheService,
|
||||
ResponseEnvelope, ScopedCache,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -73,32 +73,35 @@ pub enum CacheScope {
|
|||
Isolated(String),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CacheOptions {
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct CachePolicy {
|
||||
pub caching: Option<bool>,
|
||||
pub no_cache: bool,
|
||||
pub no_store: bool,
|
||||
pub ttl: Option<Duration>,
|
||||
pub max_age: Option<Duration>,
|
||||
}
|
||||
|
||||
impl CachePolicy {
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.caching != Some(false) && !(self.no_cache && self.no_store)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CacheOptions {
|
||||
pub policy: CachePolicy,
|
||||
pub scope: CacheScope,
|
||||
}
|
||||
|
||||
impl CacheOptions {
|
||||
pub fn new(scope: CacheScope) -> Self {
|
||||
Self {
|
||||
caching: None,
|
||||
no_cache: false,
|
||||
no_store: false,
|
||||
ttl: None,
|
||||
max_age: None,
|
||||
policy: CachePolicy::default(),
|
||||
scope,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.caching != Some(false) && !(self.no_cache && self.no_store)
|
||||
}
|
||||
|
||||
pub fn request(self, namespace: &str, surface: &str, mut input: Value) -> ResponseCacheRequest {
|
||||
input.sort_all_objects();
|
||||
let scope = match self.scope {
|
||||
|
|
@ -128,13 +131,15 @@ impl CacheOptions {
|
|||
supported_call_type: true,
|
||||
native_backend: true,
|
||||
default_on: true,
|
||||
caching: self.caching,
|
||||
no_cache: self.no_cache,
|
||||
no_store: self.no_store,
|
||||
caching: self.policy.caching,
|
||||
no_cache: self.policy.no_cache,
|
||||
no_store: self.policy.no_store,
|
||||
..Default::default()
|
||||
},
|
||||
context: ExactCacheContext { ttl: self.ttl },
|
||||
max_age: self.max_age,
|
||||
context: ExactCacheContext {
|
||||
ttl: self.policy.ttl,
|
||||
},
|
||||
max_age: self.policy.max_age,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -171,7 +176,10 @@ impl ScopedCache {
|
|||
Self { service, scope }
|
||||
}
|
||||
|
||||
pub fn options(&self, overrides: Option<CacheOptions>) -> CacheOptions {
|
||||
overrides.unwrap_or_else(|| CacheOptions::new(self.scope.clone()))
|
||||
pub fn options(&self, policy: Option<CachePolicy>) -> CacheOptions {
|
||||
CacheOptions {
|
||||
policy: policy.unwrap_or_default(),
|
||||
scope: self.scope.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,11 +129,20 @@ async fn isolated_policy_controls_actual_entry_reuse(
|
|||
#[case] first: &str,
|
||||
#[case] second: &str,
|
||||
#[case] hit: bool,
|
||||
#[values(false, true)] override_policy: bool,
|
||||
) {
|
||||
use litellm_cache_response::{CacheOptions, CacheScope};
|
||||
let service = ResponseCache::new(Arc::new(InMemoryCache::<CacheEntry>::default()));
|
||||
let request =
|
||||
|scope| CacheOptions::new(scope).request("test", "messages", json!({"prompt":"hello"}));
|
||||
use litellm_cache_response::{CachePolicy, CacheScope, ScopedCache};
|
||||
let service = Arc::new(ResponseCache::new(Arc::new(
|
||||
InMemoryCache::<CacheEntry>::default(),
|
||||
)));
|
||||
let request = |scope| {
|
||||
ScopedCache::new(service.clone(), scope)
|
||||
.options(override_policy.then_some(CachePolicy {
|
||||
ttl: Some(Duration::from_secs(30)),
|
||||
..CachePolicy::default()
|
||||
}))
|
||||
.request("test", "messages", json!({"prompt":"hello"}))
|
||||
};
|
||||
service
|
||||
.async_store(
|
||||
&request(CacheScope::Isolated(first.into())),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
- The enum only shrinks: when Rust owns a subsystem, delete its group rather than adding a Rust path beside it
|
||||
- Calling a user's own callback directly is permanent Python surface and gets its own type outside `LegacyPython`
|
||||
- `PublicCall` is the caller's call as `Logging` sees it: the positional arguments, the keyword view as the call rewrites it (setup, deployment hook, preflight) and the bound request object backing omitted keywords; shared bridge composition hands it to `LegacyLogging`; routes use the neutral call boundary
|
||||
- `LoggingOperation` selects legacy logging entrypoints and response handling. It belongs here rather than in shared inference data contracts
|
||||
- `setup` reuses a `Logging` passed as `litellm_logging_obj` (the proxy and Router) and otherwise builds one through `function_setup`; which callbacks run is `Logging`'s decision, never this crate's
|
||||
- Callbacks receive the caller's own objects and may mutate them; this crate alone carries that obligation
|
||||
- Retain complete boundary arguments, opaque values, aliases, omitted/default distinctions and deliberate copies; preserve the deployment-hook kwargs view
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ license.workspace = true
|
|||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
litellm-types.workspace = true
|
||||
litellm-host.workspace = true
|
||||
litellm-host-python.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
//! raises is answered with the same `Logging` calls, in the same order, as the Python
|
||||
//! `@client` path makes them.
|
||||
|
||||
use crate::LoggingOperation;
|
||||
use litellm_host_python::PythonOwned;
|
||||
use litellm_types::Operation;
|
||||
|
||||
use litellm_host::{
|
||||
interceptors::{RawResponse, RequestContext, WireRequest},
|
||||
|
|
@ -45,7 +45,7 @@ struct LoggedRequest {
|
|||
}
|
||||
|
||||
pub struct LegacyLogging {
|
||||
operation: Operation,
|
||||
operation: LoggingOperation,
|
||||
call: PublicCall,
|
||||
logger: Option<PythonLogger>,
|
||||
start: Py<PyAny>,
|
||||
|
|
@ -68,7 +68,12 @@ fn is_cancellation(py: Python<'_>, error: &PyErr) -> bool {
|
|||
}
|
||||
|
||||
impl LegacyLogging {
|
||||
pub fn new(py: Python<'_>, operation: Operation, call: PublicCall, asynchronous: bool) -> Self {
|
||||
pub fn new(
|
||||
py: Python<'_>,
|
||||
operation: LoggingOperation,
|
||||
call: PublicCall,
|
||||
asynchronous: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
operation,
|
||||
call,
|
||||
|
|
@ -87,32 +92,34 @@ impl LegacyLogging {
|
|||
|
||||
fn call_type(&self) -> &'static str {
|
||||
match (self.operation, self.asynchronous) {
|
||||
(Operation::Completion, false) => "completion",
|
||||
(Operation::Completion, true) => "acompletion",
|
||||
(Operation::Responses, false) => "responses",
|
||||
(Operation::Responses, true) => "aresponses",
|
||||
(Operation::Messages, _) => "anthropic_messages",
|
||||
(Operation::Ocr, false) => "ocr",
|
||||
(Operation::Ocr, true) => "aocr",
|
||||
(LoggingOperation::Completion, false) => "completion",
|
||||
(LoggingOperation::Completion, true) => "acompletion",
|
||||
(LoggingOperation::Responses, false) => "responses",
|
||||
(LoggingOperation::Responses, true) => "aresponses",
|
||||
(LoggingOperation::Messages, _) => "anthropic_messages",
|
||||
(LoggingOperation::Ocr, false) => "ocr",
|
||||
(LoggingOperation::Ocr, true) => "aocr",
|
||||
}
|
||||
}
|
||||
|
||||
fn input_description(&self) -> &'static str {
|
||||
match self.operation {
|
||||
Operation::Completion => "Chat completions",
|
||||
Operation::Responses => "Responses",
|
||||
Operation::Messages => "Messages",
|
||||
Operation::Ocr => "OCR document processing",
|
||||
LoggingOperation::Completion => "Chat completions",
|
||||
LoggingOperation::Responses => "Responses",
|
||||
LoggingOperation::Messages => "Messages",
|
||||
LoggingOperation::Ocr => "OCR document processing",
|
||||
}
|
||||
}
|
||||
|
||||
fn stream_billing(&self) -> Option<PassThroughStream> {
|
||||
match self.operation {
|
||||
Operation::Messages => Some(PassThroughStream {
|
||||
LoggingOperation::Messages => Some(PassThroughStream {
|
||||
url_route: "/v1/messages",
|
||||
endpoint_type: "anthropic",
|
||||
}),
|
||||
Operation::Completion | Operation::Responses | Operation::Ocr => None,
|
||||
LoggingOperation::Completion | LoggingOperation::Responses | LoggingOperation::Ocr => {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -643,16 +650,16 @@ kwargs = {'logger': logger, 'document': document}
|
|||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::sync_completion(litellm_types::Operation::Completion, false, "completion")]
|
||||
#[case::async_completion(litellm_types::Operation::Completion, true, "acompletion")]
|
||||
#[case::sync_responses(litellm_types::Operation::Responses, false, "responses")]
|
||||
#[case::async_responses(litellm_types::Operation::Responses, true, "aresponses")]
|
||||
#[case::sync_messages(litellm_types::Operation::Messages, false, "anthropic_messages")]
|
||||
#[case::async_messages(litellm_types::Operation::Messages, true, "anthropic_messages")]
|
||||
#[case::sync_ocr(litellm_types::Operation::Ocr, false, "ocr")]
|
||||
#[case::async_ocr(litellm_types::Operation::Ocr, true, "aocr")]
|
||||
#[case::sync_completion(crate::LoggingOperation::Completion, false, "completion")]
|
||||
#[case::async_completion(crate::LoggingOperation::Completion, true, "acompletion")]
|
||||
#[case::sync_responses(crate::LoggingOperation::Responses, false, "responses")]
|
||||
#[case::async_responses(crate::LoggingOperation::Responses, true, "aresponses")]
|
||||
#[case::sync_messages(crate::LoggingOperation::Messages, false, "anthropic_messages")]
|
||||
#[case::async_messages(crate::LoggingOperation::Messages, true, "anthropic_messages")]
|
||||
#[case::sync_ocr(crate::LoggingOperation::Ocr, false, "ocr")]
|
||||
#[case::async_ocr(crate::LoggingOperation::Ocr, true, "aocr")]
|
||||
fn operation_selects_the_legacy_setup_and_deployment_hook_contract(
|
||||
#[case] operation: litellm_types::Operation,
|
||||
#[case] operation: crate::LoggingOperation,
|
||||
#[case] asynchronous: bool,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
|
|
@ -1088,12 +1095,12 @@ check = lambda: None
|
|||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::completion(litellm_types::Operation::Completion, "Chat completions")]
|
||||
#[case::responses(litellm_types::Operation::Responses, "Responses")]
|
||||
#[case::messages(litellm_types::Operation::Messages, "Messages")]
|
||||
#[case::ocr(litellm_types::Operation::Ocr, "OCR document processing")]
|
||||
#[case::completion(crate::LoggingOperation::Completion, "Chat completions")]
|
||||
#[case::responses(crate::LoggingOperation::Responses, "Responses")]
|
||||
#[case::messages(crate::LoggingOperation::Messages, "Messages")]
|
||||
#[case::ocr(crate::LoggingOperation::Ocr, "OCR document processing")]
|
||||
fn prepared_arguments_replace_the_legacy_view_without_losing_callback_aliases(
|
||||
#[case] operation: litellm_types::Operation,
|
||||
#[case] operation: crate::LoggingOperation,
|
||||
#[case] description: &str,
|
||||
) {
|
||||
Python::initialize();
|
||||
|
|
@ -1763,7 +1770,7 @@ assert logger.calls[1][1] is response
|
|||
Python::attach(|py| {
|
||||
let locals = namespace(py, c"first = b'first'\nlast = b'last'\nresponse = None");
|
||||
let mut logging = LegacyLogging {
|
||||
operation: litellm_types::Operation::Messages,
|
||||
operation: crate::LoggingOperation::Messages,
|
||||
..logged(py, &locals, true)
|
||||
};
|
||||
logging
|
||||
|
|
|
|||
|
|
@ -20,5 +20,13 @@ pub(crate) use callbacks::{LegacyCallbacks, is_internal_call};
|
|||
pub(crate) use logger::{DeploymentHooks, PythonLogger, finalize, setup};
|
||||
pub use mapping::{CallBoundary, CallbackMapping, Dispatch, callback_mappings};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LoggingOperation {
|
||||
Completion,
|
||||
Responses,
|
||||
Messages,
|
||||
Ocr,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_support;
|
||||
|
|
|
|||
|
|
@ -189,5 +189,5 @@ pub(crate) fn legacy_call(
|
|||
.map(|kwargs| kwargs.cast_into::<PyDict>().unwrap())
|
||||
.unwrap_or_else(|| PyDict::new(py));
|
||||
let call = PublicCall::capture(&request, &PyTuple::empty(py), &kwargs).unwrap();
|
||||
LegacyLogging::new(py, litellm_types::Operation::Ocr, call, asynchronous)
|
||||
LegacyLogging::new(py, crate::LoggingOperation::Ocr, call, asynchronous)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,10 @@ repository.workspace = true
|
|||
[dependencies]
|
||||
fancy-regex.workspace = true
|
||||
litellm-tracing.workspace = true
|
||||
litellm-types.workspace = true
|
||||
litellm-llms-types.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_path_to_error = "0.1"
|
||||
serde_with.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
url.workspace = true
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use litellm_types::utils::{ChatCompletionsUsage, PromptTokensDetails};
|
||||
use litellm_llms_types::formats::chat_completions::{ChatCompletionsUsage, PromptTokensDetails};
|
||||
|
||||
/// OpenAI finish reasons, mirroring Python's `_FINISH_REASON_MAP` for the
|
||||
/// reasons the providers on this route can emit. Python warns and falls back to
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use litellm_types::utils::{ProviderSpecificHeader, ProviderSpecificHeaders};
|
||||
use litellm_llms_types::headers::{ProviderSpecificHeader, ProviderSpecificHeaders};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
pub fn get_provider_specific_headers(
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
//! `_bedrock_converse_messages_pt` for the text-only surface this route
|
||||
//! accepts; anything richer is declined upstream by the capability gate.
|
||||
|
||||
use litellm_types::llms::openai::{ChatMessage, ChatMessageContent};
|
||||
use litellm_llms_types::formats::chat_completions::{ChatMessage, ChatMessageContent};
|
||||
use strum::IntoStaticStr;
|
||||
|
||||
pub const EMPTY_TEXT_PLACEHOLDER: &str =
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
use serde::{
|
||||
Deserializer,
|
||||
de::{Error, Visitor},
|
||||
};
|
||||
use serde_with::DeserializeAs;
|
||||
|
||||
pub struct LaxI64;
|
||||
pub struct FiniteF64;
|
||||
|
||||
pub fn parse_str_bool(value: &str) -> Option<bool> {
|
||||
let token = value.trim_matches(|character: char| {
|
||||
character.is_whitespace() || matches!(character, '\u{1c}'..='\u{1f}')
|
||||
|
|
@ -22,129 +13,12 @@ pub fn parse_redis_bool(value: &str) -> bool {
|
|||
value == "1" || value.eq_ignore_ascii_case("true") || value.eq_ignore_ascii_case("yes")
|
||||
}
|
||||
|
||||
impl<'de> DeserializeAs<'de, i64> for LaxI64 {
|
||||
fn deserialize_as<D: Deserializer<'de>>(deserializer: D) -> Result<i64, D::Error> {
|
||||
deserializer.deserialize_any(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Visitor<'de> for LaxI64 {
|
||||
type Value = i64;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("an integer in the i64 range")
|
||||
}
|
||||
|
||||
fn visit_i64<E: Error>(self, value: i64) -> Result<i64, E> {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn visit_u64<E: Error>(self, value: u64) -> Result<i64, E> {
|
||||
i64::try_from(value).map_err(E::custom)
|
||||
}
|
||||
|
||||
fn visit_f64<E: Error>(self, value: f64) -> Result<i64, E> {
|
||||
integral_float(value).ok_or_else(|| E::custom("expected an integer in the i64 range"))
|
||||
}
|
||||
|
||||
fn visit_str<E: Error>(self, value: &str) -> Result<i64, E> {
|
||||
integer_string(value.trim())
|
||||
.ok_or_else(|| E::custom("expected an integer in the i64 range"))
|
||||
}
|
||||
|
||||
fn visit_bool<E: Error>(self, value: bool) -> Result<i64, E> {
|
||||
Ok(i64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeAs<'de, f64> for FiniteF64 {
|
||||
fn deserialize_as<D: Deserializer<'de>>(deserializer: D) -> Result<f64, D::Error> {
|
||||
deserializer.deserialize_any(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Visitor<'de> for FiniteF64 {
|
||||
type Value = f64;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("a finite number")
|
||||
}
|
||||
|
||||
fn visit_i64<E: Error>(self, value: i64) -> Result<f64, E> {
|
||||
Ok(value as f64)
|
||||
}
|
||||
|
||||
fn visit_u64<E: Error>(self, value: u64) -> Result<f64, E> {
|
||||
Ok(value as f64)
|
||||
}
|
||||
|
||||
fn visit_f64<E: Error>(self, value: f64) -> Result<f64, E> {
|
||||
value
|
||||
.is_finite()
|
||||
.then_some(value)
|
||||
.ok_or_else(|| E::custom("expected a finite number"))
|
||||
}
|
||||
|
||||
fn visit_str<E: Error>(self, value: &str) -> Result<f64, E> {
|
||||
self.visit_f64(value.trim().parse::<f64>().map_err(E::custom)?)
|
||||
}
|
||||
|
||||
fn visit_bool<E: Error>(self, value: bool) -> Result<f64, E> {
|
||||
Ok(f64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
fn integer_string(value: &str) -> Option<i64> {
|
||||
let integer = match value.split_once('.') {
|
||||
Some((integer, fraction)) => {
|
||||
if fraction.is_empty() || !fraction.bytes().all(|byte| byte == b'0') {
|
||||
return None;
|
||||
}
|
||||
integer
|
||||
}
|
||||
None => value,
|
||||
};
|
||||
if integer.starts_with('_') || integer.ends_with('_') || integer.contains("__") {
|
||||
return None;
|
||||
}
|
||||
let digits = integer.strip_prefix(['+', '-']).unwrap_or(integer);
|
||||
if digits.is_empty()
|
||||
|| digits.starts_with('_')
|
||||
|| !digits
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_digit() || byte == b'_')
|
||||
{
|
||||
return None;
|
||||
}
|
||||
integer.replace('_', "").parse().ok()
|
||||
}
|
||||
|
||||
fn integral_float(value: f64) -> Option<i64> {
|
||||
(value.is_finite()
|
||||
&& value.fract() == 0.0
|
||||
&& value >= i64::MIN as f64
|
||||
&& value < -(i64::MIN as f64))
|
||||
.then_some(value as i64)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serde_with::serde_as;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
struct Numbers {
|
||||
#[serde_as(deserialize_as = "Option<Vec<LaxI64>>")]
|
||||
integers: Option<Vec<i64>>,
|
||||
#[serde_as(deserialize_as = "Option<FiniteF64>")]
|
||||
float: Option<f64>,
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::trimmed_true(" True ", Some(true))]
|
||||
#[case::control_whitespace_true("\u{1c}TRUE\u{1f}", Some(true))]
|
||||
|
|
@ -160,73 +34,4 @@ mod tests {
|
|||
) {
|
||||
assert_eq!(parse_str_bool(input), expected, "{input:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapters_compose_and_serialize_as_numbers() {
|
||||
let numbers: Numbers = serde_json::from_value(json!({
|
||||
"integers": ["9007199254740993.0", "1_000", " +2.000 ", 3.0, true],
|
||||
"float": " 1.5 "
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(numbers).unwrap(),
|
||||
json!({
|
||||
"integers": [9_007_199_254_740_993_i64, 1000, 2, 3, 1], "float": 1.5
|
||||
})
|
||||
);
|
||||
for input in [json!({}), json!({"integers": null, "float": null})] {
|
||||
assert_eq!(
|
||||
serde_json::from_value::<Numbers>(input).unwrap(),
|
||||
Numbers {
|
||||
integers: None,
|
||||
float: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integer_bounds_and_invalid_values_are_checked() {
|
||||
for input in [
|
||||
json!(i64::MIN),
|
||||
json!(i64::MAX),
|
||||
json!(i64::MAX.to_string()),
|
||||
] {
|
||||
assert!(serde_json::from_value::<Numbers>(json!({"integers": [input]})).is_ok());
|
||||
}
|
||||
for input in [
|
||||
json!(u64::MAX),
|
||||
json!(9_223_372_036_854_775_808_u64),
|
||||
json!(9_223_372_036_854_775_808.0),
|
||||
json!("-9223372036854775809"),
|
||||
json!("1.0000000000000001"),
|
||||
json!("1e3"),
|
||||
json!("2."),
|
||||
json!(".0"),
|
||||
json!("_2"),
|
||||
json!("2__0"),
|
||||
json!(2.5),
|
||||
json!(null),
|
||||
json!({}),
|
||||
] {
|
||||
assert!(serde_json::from_value::<Numbers>(json!({"integers": [input]})).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floats_reject_nonfinite_and_invalid_values() {
|
||||
for input in [
|
||||
json!("NaN"),
|
||||
json!("inf"),
|
||||
json!("-inf"),
|
||||
json!("1e999"),
|
||||
json!([]),
|
||||
] {
|
||||
assert!(serde_json::from_value::<Numbers>(json!({"float": input})).is_err());
|
||||
}
|
||||
for (input, expected) in [(json!(2), 2.0), (json!(2.5), 2.5), (json!(true), 1.0)] {
|
||||
let numbers: Numbers = serde_json::from_value(json!({"float": input})).unwrap();
|
||||
assert_eq!(numbers.float, Some(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ Responses WebSocket sessions remain separate from the HTTP call driver because a
|
|||
|
||||
## Crate layering
|
||||
|
||||
For Messages, Responses, Chat Completions, OCR, and other API formats, `core/src/<format>/` owns orchestration. Shared API data contracts belong in `litellm-types`, adapter contracts and shared transformation machinery in `llms/src/base_llm/<format>/`, and provider policy in `llms/src/<provider>/<format>/`. A repeated format directory name does not imply interchangeable responsibilities. Select concrete adapters here, then invoke their contracts instead of applying one provider's policy to every call. Route types describe call envelopes and execution state, not duplicate public payload schemas
|
||||
For Messages, Responses, Chat Completions, OCR, and other API formats, `core/src/<format>/` owns orchestration. Shared API data contracts belong in `litellm-llms-types`, adapter contracts and shared transformation machinery in `llms/src/base_llm/<format>/`, and provider policy in `llms/src/<provider>/<format>/`. A repeated format directory name does not imply interchangeable responsibilities. Select concrete adapters here, then invoke their contracts instead of applying one provider's policy to every call. Route types describe call envelopes and execution state, not duplicate public payload schemas
|
||||
|
||||
Each crate mirrors one top-level Python package, so a Rust path reads as its Python path with the crate name in place of the package directory. Dependencies only point down:
|
||||
Crates separate API data, transformations, transport, and orchestration. Python package names identify counterparts, not ownership. Dependencies only point down:
|
||||
|
||||
- `litellm-types` mirrors `litellm/types/`: pure serde data, no I/O
|
||||
- `litellm-llms-types` owns shared inference API contracts, grouped by format: pure serde data and shape validation, no I/O
|
||||
- `litellm-core-utils` mirrors `litellm/litellm_core_utils/`: pure helpers (provider resolution, prompt factory, call arguments, settings lookup and layer merge), no network I/O
|
||||
- `litellm-http` is Rust-only and route-neutral: settings resolution, the pooled `reqwest` clients, TLS, proxies, the SSRF-safe media fetcher, request and header helpers, and transport errors. Python's `litellm/llms/custom_httpx/` is split by responsibility instead of mirrored: its transport half lives here, its OCR handler in `litellm-llms`
|
||||
- `litellm-llms` mirrors `litellm/llms/`: `base_llm/<api>/transformation.rs`, `<provider>/<api>/transformation.rs`, and `base_llm/ocr/handler.rs` (the OCR request handler)
|
||||
|
|
@ -36,7 +36,9 @@ Not here: serving HTTP (axum routes, extractors), config file reading, rollout s
|
|||
|
||||
## Response caching and accounting boundary
|
||||
|
||||
Attach a `litellm_cache_response::ScopedCache` with `route.with_cache(cache)`. Cached and uncached routes use the same `execute` and `machine` methods. `CallOptions` carries per-call cache overrides and observation; attaching a service does not change the execution contract
|
||||
Attach a `litellm_cache_response::ScopedCache` with `route.with_cache(cache)`. Cached and uncached routes use the same `execute` and `machine` methods. `CallOptions` carries a scope-free `CachePolicy` and observation; per-call policy never replaces the attached scope or service
|
||||
|
||||
Messages groups per-call dependencies in `CallContext` and explicitly sequences cache lookup, provider execution, result acceptance, and cache storage. Provider transport does not own cache orchestration. Stream capture remains in the shared cache implementation
|
||||
|
||||
Core owns request identity, typed response reconstruction and stream capture/replay. `cache-response` owns cache policy, namespacing, scope encoding, versioned envelopes and freshness. The SDK explicitly chooses shared scope. The gateway derives isolated scope from authenticated identity before attaching its service
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ litellm-cache-response.workspace = true
|
|||
litellm-framing.workspace = true
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
litellm-secrets.workspace = true
|
||||
litellm-types.workspace = true
|
||||
litellm-llms-types.workspace = true
|
||||
litellm-core-utils.workspace = true
|
||||
litellm-host.workspace = true
|
||||
bytes.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::{
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
|
@ -7,7 +8,8 @@ use std::{
|
|||
use bytes::{Bytes, BytesMut};
|
||||
use futures_util::{StreamExt, TryStreamExt, stream};
|
||||
use litellm_cache_response::{
|
||||
CacheOptions, ResponseCacheRequest, ResponseCacheService, ResponseEnvelope, cache_key,
|
||||
CacheOptions, CachePolicy, ResponseCacheRequest, ResponseCacheService, ResponseEnvelope,
|
||||
ScopedCache, cache_key,
|
||||
};
|
||||
use litellm_host::{
|
||||
call::{CallOutput, OutputOf},
|
||||
|
|
@ -77,7 +79,7 @@ impl CacheSession {
|
|||
options: Option<CacheOptions>,
|
||||
request: &CacheRequest,
|
||||
) -> Option<Self> {
|
||||
let options = options.filter(CacheOptions::enabled)?;
|
||||
let options = options.filter(|options| options.policy.enabled())?;
|
||||
let service = service?;
|
||||
let input = request.input.clone();
|
||||
let request = options.request(&service.config().namespace, P::SURFACE, input);
|
||||
|
|
@ -198,81 +200,137 @@ where
|
|||
let identity = request.identity.clone();
|
||||
crate::diagnostic::provider(&identity.model, &identity.provider);
|
||||
let session = CacheSession::prepare::<P>(cache, options, &request);
|
||||
let hit = match &session {
|
||||
Some(session) => session.lookup::<P>().await.and_then(|entry| {
|
||||
let output = match entry {
|
||||
CachedOutput::Response(response) => Some(CallOutput::Complete(response)),
|
||||
CachedOutput::Stream(data) => P::replay(Bytes::from(data)),
|
||||
};
|
||||
output.map(|output| (output, cache_key(&session.request.key)))
|
||||
}),
|
||||
None => None,
|
||||
let cache = CallCache::<P> {
|
||||
session,
|
||||
protocol: PhantomData,
|
||||
};
|
||||
let hit = cache.lookup().await;
|
||||
let (output, source) = match hit {
|
||||
Some((output, key)) => (output, ResultSource::Cache { key }),
|
||||
Some(hit) => hit,
|
||||
None => (provider().await?, ResultSource::Provider),
|
||||
};
|
||||
let from_provider = source == ResultSource::Provider;
|
||||
publish(
|
||||
ExecutionFacts {
|
||||
provider: identity,
|
||||
source,
|
||||
source: source.clone(),
|
||||
},
|
||||
interceptors,
|
||||
observers,
|
||||
)
|
||||
.await?;
|
||||
let Some(session) =
|
||||
session.filter(|session| from_provider && session.request.controls.writes())
|
||||
else {
|
||||
return Ok(output);
|
||||
};
|
||||
match output {
|
||||
CallOutput::Complete(response) => {
|
||||
session.store_response::<P>(&response).await;
|
||||
Ok(CallOutput::Complete(response))
|
||||
}
|
||||
CallOutput::Stream { head, chunks } => {
|
||||
let captured = stream::try_unfold(
|
||||
(chunks, Some(Vec::<u8>::new()), session),
|
||||
|(mut chunks, captured, session)| async move {
|
||||
match chunks.try_next().await? {
|
||||
Some(chunk) => {
|
||||
let captured = captured.and_then(|mut data| {
|
||||
let bytes = P::bytes(&chunk);
|
||||
if data.len().saturating_add(bytes.len())
|
||||
> session.service.config().max_entry_bytes
|
||||
{
|
||||
return None;
|
||||
}
|
||||
data.extend_from_slice(bytes);
|
||||
Some(data)
|
||||
});
|
||||
Ok(Some((chunk, (chunks, captured, session))))
|
||||
}
|
||||
None => {
|
||||
if let Some(data) = captured
|
||||
&& let Ok(text) = String::from_utf8(data)
|
||||
&& successful_stream(&text, P::TERMINAL_EVENT)
|
||||
&& let Ok(entry) = serde_json::to_value(ResponseEnvelope::new(
|
||||
P::SURFACE,
|
||||
CachedOutput::<Value>::Stream(text),
|
||||
))
|
||||
{
|
||||
session.store(entry).await;
|
||||
}
|
||||
Ok::<_, RouteError>(None)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.boxed();
|
||||
Ok(CallOutput::Stream {
|
||||
head,
|
||||
chunks: captured,
|
||||
Ok(cache.finish(output, &source).await)
|
||||
}
|
||||
|
||||
pub(crate) struct CallCache<P> {
|
||||
session: Option<CacheSession>,
|
||||
protocol: PhantomData<P>,
|
||||
}
|
||||
|
||||
impl<P: StreamCachable> CallCache<P> {
|
||||
pub(crate) fn from_wire(
|
||||
cache: Option<&ScopedCache>,
|
||||
policy: CachePolicy,
|
||||
identity: &ProviderIdentity,
|
||||
wire: &WireRequest,
|
||||
) -> Self {
|
||||
let session = cache.and_then(|cache| {
|
||||
if !policy.enabled() {
|
||||
return None;
|
||||
}
|
||||
let options = cache.options(Some(policy));
|
||||
let request = CacheRequest::from_wire(identity.clone(), Some(wire));
|
||||
Some(CacheSession {
|
||||
request: options.request(
|
||||
&cache.service.config().namespace,
|
||||
P::SURFACE,
|
||||
request.input,
|
||||
),
|
||||
service: cache.service.clone(),
|
||||
})
|
||||
});
|
||||
Self {
|
||||
session,
|
||||
protocol: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn lookup(&self) -> Option<(OutputOf<P>, ResultSource)>
|
||||
where
|
||||
P::Response: DeserializeOwned,
|
||||
{
|
||||
let session = self.session.as_ref()?;
|
||||
let output = match session.lookup::<P>().await? {
|
||||
CachedOutput::Response(response) => CallOutput::Complete(response),
|
||||
CachedOutput::Stream(data) => P::replay(Bytes::from(data))?,
|
||||
};
|
||||
Some((
|
||||
output,
|
||||
ResultSource::Cache {
|
||||
key: cache_key(&session.request.key),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn finish(self, output: OutputOf<P>, source: &ResultSource) -> OutputOf<P>
|
||||
where
|
||||
P::Response: Serialize,
|
||||
{
|
||||
let Some(session) = self.session.filter(|session| {
|
||||
*source == ResultSource::Provider && session.request.controls.writes()
|
||||
}) else {
|
||||
return output;
|
||||
};
|
||||
match output {
|
||||
CallOutput::Complete(response) => {
|
||||
session.store_response::<P>(&response).await;
|
||||
CallOutput::Complete(response)
|
||||
}
|
||||
CallOutput::Stream { head, chunks } => CallOutput::Stream {
|
||||
head,
|
||||
chunks: capture_stream::<P>(chunks, session),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn capture_stream<P: StreamCachable>(
|
||||
chunks: futures_util::stream::BoxStream<'static, Result<P::Chunk, RouteError>>,
|
||||
session: CacheSession,
|
||||
) -> futures_util::stream::BoxStream<'static, Result<P::Chunk, RouteError>> {
|
||||
stream::try_unfold(
|
||||
(chunks, Some(Vec::<u8>::new()), session),
|
||||
|(mut chunks, captured, session)| async move {
|
||||
match chunks.try_next().await? {
|
||||
Some(chunk) => {
|
||||
let captured = captured.and_then(|mut data| {
|
||||
let bytes = P::bytes(&chunk);
|
||||
if data.len().saturating_add(bytes.len())
|
||||
> session.service.config().max_entry_bytes
|
||||
{
|
||||
return None;
|
||||
}
|
||||
data.extend_from_slice(bytes);
|
||||
Some(data)
|
||||
});
|
||||
Ok(Some((chunk, (chunks, captured, session))))
|
||||
}
|
||||
None => {
|
||||
if let Some(data) = captured
|
||||
&& let Ok(text) = String::from_utf8(data)
|
||||
&& successful_stream(&text, P::TERMINAL_EVENT)
|
||||
&& let Ok(entry) = serde_json::to_value(ResponseEnvelope::new(
|
||||
P::SURFACE,
|
||||
CachedOutput::<Value>::Stream(text),
|
||||
))
|
||||
{
|
||||
session.store(entry).await;
|
||||
}
|
||||
Ok::<_, RouteError>(None)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn now() -> Duration {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use litellm_host::lifecycle::ExecutionEvent;
|
||||
use litellm_host::observation::ObservationSender;
|
||||
use litellm_host::{lifecycle::ExecutionEvent, observation::ObservationSender};
|
||||
use std::time::Duration;
|
||||
|
||||
use litellm_auth::AuthServices;
|
||||
|
|
@ -9,7 +8,7 @@ use litellm_llms::base_llm::{
|
|||
auth::{Authenticated, resolve_auth},
|
||||
chat::transformation::ProviderChatResponseData,
|
||||
};
|
||||
use litellm_types::utils::ChatCompletionsResponse;
|
||||
use litellm_llms_types::formats::chat_completions::ChatCompletionsResponse;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::Error;
|
||||
|
|
@ -23,7 +22,7 @@ pub(super) async fn execute(
|
|||
auth: &AuthServices,
|
||||
request: ProviderChatCompletionsRequest,
|
||||
cache: Option<litellm_cache_response::ScopedCache>,
|
||||
cache_options: Option<litellm_cache_response::CacheOptions>,
|
||||
cache_options: Option<litellm_cache_response::CachePolicy>,
|
||||
interceptors: &impl Interceptors<Error>,
|
||||
observers: Option<&ObservationSender>,
|
||||
) -> Result<ChatCompletionsResponse, Error> {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ pub use crate::error::RouteError as Error;
|
|||
mod common_utils;
|
||||
pub(crate) mod handler;
|
||||
mod prepare;
|
||||
use litellm_types::utils::ChatCompletionsResponse;
|
||||
use litellm_llms_types::formats::chat_completions::ChatCompletionsResponse;
|
||||
use prepare::{prepare_provider_request, resolve_request};
|
||||
|
||||
use crate::chat_completions::types::ChatCompletionsRequest;
|
||||
|
|
@ -67,7 +67,7 @@ impl ChatCompletionsRoute {
|
|||
async fn run(
|
||||
&self,
|
||||
request: ChatCompletionsRequest<'_>,
|
||||
cache_options: Option<litellm_cache_response::CacheOptions>,
|
||||
cache_options: Option<litellm_cache_response::CachePolicy>,
|
||||
interceptors: &impl litellm_host::interceptors::Interceptors<Error>,
|
||||
observers: Option<&ObservationSender>,
|
||||
) -> Result<ChatCompletionsResponse, Error> {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ use litellm_auth::SecretValue;
|
|||
use litellm_core_utils::settings::Lookup;
|
||||
use litellm_http::request::with_default_headers;
|
||||
use litellm_llms::base_llm::{auth::ValidatedEnvironment, chat::transformation::BaseConfig};
|
||||
use litellm_llms_types::formats::chat_completions::ChatMessage;
|
||||
use litellm_secrets::source::Secrets;
|
||||
use litellm_types::llms::openai::ChatMessage;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use litellm_host::{
|
|||
call::{CallOutput, HostedMachine, hosted_call},
|
||||
protocol::Protocol,
|
||||
};
|
||||
use litellm_types::utils::ChatCompletionsResponse;
|
||||
use litellm_llms_types::formats::chat_completions::ChatCompletionsResponse;
|
||||
|
||||
use super::{
|
||||
ChatCompletionsRoute, Error,
|
||||
|
|
@ -55,7 +55,7 @@ impl ChatCompletionsRoute {
|
|||
pub(super) async fn run_call(
|
||||
&self,
|
||||
call: ChatCompletionsCall,
|
||||
cache_options: Option<litellm_cache_response::CacheOptions>,
|
||||
cache_options: Option<litellm_cache_response::CachePolicy>,
|
||||
interceptors: &impl litellm_host::interceptors::Interceptors<Error>,
|
||||
observers: Option<&ObservationSender>,
|
||||
) -> Result<ChatCompletionsResponse, Error> {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::time::Duration;
|
|||
|
||||
use litellm_auth::SecretValue;
|
||||
use litellm_llms::base_llm::{auth::ValidatedEnvironment, chat::transformation::BaseConfig};
|
||||
use litellm_types::llms::openai::ChatMessage;
|
||||
use litellm_llms_types::formats::chat_completions::ChatMessage;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
/// A `/chat/completions` call as it crosses into the core.
|
||||
|
|
|
|||
48
litellm-rust/crates/core/src/context.rs
Normal file
48
litellm-rust/crates/core/src/context.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use litellm_cache_response::CachePolicy;
|
||||
use litellm_host::{
|
||||
interceptors::{ExecutionFacts, Interceptors, RawResponse},
|
||||
lifecycle::{CallEvent, ExecutionEvent},
|
||||
observation::ObservationSender,
|
||||
};
|
||||
|
||||
use crate::{CallOptions, RouteError};
|
||||
|
||||
pub(crate) struct CallContext<'a, I> {
|
||||
pub interceptors: &'a I,
|
||||
pub observers: Option<ObservationSender>,
|
||||
pub cache: CachePolicy,
|
||||
}
|
||||
|
||||
impl<'a, I: Interceptors<RouteError>> CallContext<'a, I> {
|
||||
pub fn new(interceptors: &'a I, options: CallOptions) -> Self {
|
||||
Self {
|
||||
interceptors,
|
||||
observers: options.observers,
|
||||
cache: options.cache.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn result_ready(&self, facts: ExecutionFacts) -> Result<(), RouteError> {
|
||||
if let Some(observers) = &self.observers {
|
||||
observers.emit(CallEvent::Execution(ExecutionEvent::ResultReady {
|
||||
facts: facts.clone(),
|
||||
}));
|
||||
}
|
||||
self.interceptors.result_ready(facts).await
|
||||
}
|
||||
|
||||
pub async fn response_received(&self, body: &str) -> Result<(), RouteError> {
|
||||
let raw = RawResponse {
|
||||
body: body.to_owned(),
|
||||
};
|
||||
if let Some(observers) = &self.observers {
|
||||
observers.emit(CallEvent::Execution(
|
||||
ExecutionEvent::ProviderResponseReceived { raw: raw.clone() },
|
||||
));
|
||||
}
|
||||
self.interceptors
|
||||
.after_provider_response(raw)
|
||||
.await
|
||||
.map_err(RouteError::post_call)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
mod context;
|
||||
mod diagnostic;
|
||||
|
||||
pub mod audio_transcription;
|
||||
|
|
@ -16,7 +17,7 @@ pub use error::RouteError;
|
|||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CallOptions {
|
||||
pub cache: Option<litellm_cache_response::CacheOptions>,
|
||||
pub cache: Option<litellm_cache_response::CachePolicy>,
|
||||
pub observers: Option<litellm_host::observation::ObservationSender>,
|
||||
}
|
||||
|
||||
|
|
@ -29,8 +30,8 @@ impl From<Option<litellm_host::observation::ObservationSender>> for CallOptions
|
|||
}
|
||||
}
|
||||
|
||||
impl From<litellm_cache_response::CacheOptions> for CallOptions {
|
||||
fn from(cache: litellm_cache_response::CacheOptions) -> Self {
|
||||
impl From<litellm_cache_response::CachePolicy> for CallOptions {
|
||||
fn from(cache: litellm_cache_response::CachePolicy) -> Self {
|
||||
Self {
|
||||
cache: Some(cache),
|
||||
observers: None,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
This directory owns provider-independent Messages call orchestration: the entrypoint, call envelopes, provider selection, credential resolution, transport coordination, hooks, and stream lifecycle. Shared API data contracts belong in `litellm-types::messages`, adapter contracts and execution inputs in `llms/src/base_llm/messages`, and provider implementations in `llms/src/<provider>/messages`
|
||||
This directory owns provider-independent Messages call orchestration: the entrypoint, call envelopes, provider selection, credential resolution, transport coordination, hooks, and stream lifecycle. Shared API data contracts belong in `litellm-llms-types::formats::messages`, adapter contracts and execution inputs in `llms/src/base_llm/messages`, and provider implementations in `llms/src/<provider>/messages`
|
||||
|
||||
Select concrete provider adapters and invoke their contracts. Delegate authentication policy, beta selection, payload rewriting, and response interpretation to those adapters. Keep provider policy out of request preparation and transport handlers. Calling a concrete provider helper for every provider is still a policy dependency
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ pub(super) use litellm_http::request::truncate_error_body;
|
|||
use litellm_llms::{
|
||||
anthropic::messages::transformation::ANTHROPIC_MESSAGES_CONFIG,
|
||||
azure_ai::messages::transformation::AZURE_ANTHROPIC_MESSAGES_CONFIG,
|
||||
base_llm::messages::transformation::BaseAnthropicMessagesConfig,
|
||||
base_llm::messages::transformation::BaseMessagesConfig,
|
||||
bedrock::messages::invoke_transformations::anthropic_claude3_transformation::BEDROCK_ANTHROPIC_MESSAGES_CONFIG,
|
||||
};
|
||||
use serde_json::{Map, Value};
|
||||
|
|
@ -30,7 +30,7 @@ impl MessagesProvider {
|
|||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn config(self) -> &'static dyn BaseAnthropicMessagesConfig {
|
||||
pub(crate) fn config(self) -> &'static dyn BaseMessagesConfig {
|
||||
match self {
|
||||
Self::Anthropic => &ANTHROPIC_MESSAGES_CONFIG,
|
||||
Self::AzureAi => &AZURE_ANTHROPIC_MESSAGES_CONFIG,
|
||||
|
|
|
|||
|
|
@ -1,119 +1,145 @@
|
|||
use litellm_host::lifecycle::ExecutionEvent;
|
||||
use litellm_host::observation::ObservationSender;
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_util::{StreamExt, TryStreamExt, stream::BoxStream};
|
||||
use litellm_auth::AuthServices;
|
||||
use litellm_host::interceptors::{Interceptors, RawResponse, RequestContext, WireRequest};
|
||||
use litellm_host::interceptors::{Interceptors, ProviderIdentity, RequestContext, WireRequest};
|
||||
use litellm_http::transport::Error as TransportError;
|
||||
use litellm_llms::base_llm::{
|
||||
auth::{Authenticated, resolve_auth},
|
||||
messages::{
|
||||
streaming::{ByteStream, StreamDecoder, encode_anthropic_sse},
|
||||
transformation::BaseAnthropicMessagesConfig,
|
||||
transformation::BaseMessagesConfig,
|
||||
},
|
||||
};
|
||||
use litellm_llms_types::formats::messages::MessagesResponse;
|
||||
use litellm_tracing::ByteChunk;
|
||||
use litellm_types::llms::anthropic_messages::anthropic_response::AnthropicMessagesResponse;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{
|
||||
Error, MessagesResponse, common_utils::truncate_error_body, prepare::ProviderMessagesRequest,
|
||||
Error, MessagesCallResponse, MessagesRoute, common_utils::truncate_error_body,
|
||||
prepare::ProviderMessagesRequest,
|
||||
};
|
||||
use crate::{constants::MESSAGES_TIMEOUT_SECS, outbound::outbound_request};
|
||||
use crate::{constants::MESSAGES_TIMEOUT_SECS, context::CallContext, outbound::outbound_request};
|
||||
|
||||
pub(super) async fn execute(
|
||||
http: &litellm_http::Client,
|
||||
auth: &AuthServices,
|
||||
request: ProviderMessagesRequest,
|
||||
cache: Option<litellm_cache_response::ScopedCache>,
|
||||
cache_options: Option<litellm_cache_response::CacheOptions>,
|
||||
interceptors: &impl Interceptors<Error>,
|
||||
observers: Option<&ObservationSender>,
|
||||
) -> Result<MessagesResponse, Error> {
|
||||
let ProviderMessagesRequest {
|
||||
provider,
|
||||
url,
|
||||
body,
|
||||
environment,
|
||||
timeout,
|
||||
api_key,
|
||||
} = request;
|
||||
let stream = body.params.stream == Some(true);
|
||||
let context = RequestContext {
|
||||
model: body.model.clone(),
|
||||
custom_llm_provider: provider.as_str().to_string(),
|
||||
optional_params: serde_json::to_value(&body.params).map_err(serialize_failure)?,
|
||||
secret_fields: Vec::new(),
|
||||
api_key,
|
||||
};
|
||||
let authenticated = resolve_auth(auth, environment, &|key| std::env::var(key).ok()).await?;
|
||||
let identity = litellm_host::interceptors::ProviderIdentity {
|
||||
model: context.model.clone(),
|
||||
provider: context.custom_llm_provider.clone(),
|
||||
};
|
||||
let wire = interceptors
|
||||
.before_provider_request(
|
||||
WireRequest {
|
||||
url,
|
||||
headers: authenticated.headers,
|
||||
body: serde_json::to_value(&body).map_err(serialize_failure)?,
|
||||
},
|
||||
context,
|
||||
)
|
||||
.await?;
|
||||
let cache = cache.filter(|_| authenticated.signer.is_none());
|
||||
let cache_request =
|
||||
crate::caching::CacheRequest::from_wire(identity, cache.as_ref().map(|_| &wire));
|
||||
crate::caching::execute_streaming::<super::route::Messages, _, _>(
|
||||
cache_request,
|
||||
cache.as_ref().map(|cache| cache.service.clone()),
|
||||
cache.as_ref().map(|cache| cache.options(cache_options)),
|
||||
interceptors,
|
||||
observers,
|
||||
|| async move {
|
||||
let provider_name = provider.as_str();
|
||||
log_request_body(provider_name, stream, &wire.body);
|
||||
let response = send(
|
||||
http,
|
||||
Authenticated {
|
||||
headers: wire.headers,
|
||||
signer: authenticated.signer,
|
||||
pub(super) struct ProviderCall {
|
||||
pub identity: ProviderIdentity,
|
||||
pub wire: WireRequest,
|
||||
provider: super::common_utils::MessagesProvider,
|
||||
signer: Option<litellm_auth_aws::SigV4Signer>,
|
||||
timeout: Option<Duration>,
|
||||
stream: bool,
|
||||
}
|
||||
|
||||
impl ProviderCall {
|
||||
pub fn cacheable(&self) -> bool {
|
||||
self.signer.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl MessagesRoute {
|
||||
pub(super) async fn prepare_outbound(
|
||||
&self,
|
||||
request: ProviderMessagesRequest,
|
||||
context: &CallContext<'_, impl Interceptors<Error>>,
|
||||
) -> Result<ProviderCall, Error> {
|
||||
let ProviderMessagesRequest {
|
||||
provider,
|
||||
url,
|
||||
body,
|
||||
environment,
|
||||
timeout,
|
||||
api_key,
|
||||
} = request;
|
||||
let request_context = RequestContext {
|
||||
model: body.model.clone(),
|
||||
custom_llm_provider: provider.as_str().to_string(),
|
||||
optional_params: serde_json::to_value(&body.params).map_err(serialize_failure)?,
|
||||
secret_fields: Vec::new(),
|
||||
api_key,
|
||||
};
|
||||
let authenticated =
|
||||
resolve_auth(&self.auth, environment, &|key| std::env::var(key).ok()).await?;
|
||||
let identity = ProviderIdentity {
|
||||
model: request_context.model.clone(),
|
||||
provider: request_context.custom_llm_provider.clone(),
|
||||
};
|
||||
let wire = context
|
||||
.interceptors
|
||||
.before_provider_request(
|
||||
WireRequest {
|
||||
url,
|
||||
headers: authenticated.headers,
|
||||
body: serde_json::to_value(&body).map_err(serialize_failure)?,
|
||||
},
|
||||
&wire.url,
|
||||
&wire.body,
|
||||
timeout,
|
||||
request_context,
|
||||
)
|
||||
.await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(provider_error(response).await);
|
||||
}
|
||||
let config = provider.config();
|
||||
if stream {
|
||||
return Ok(streaming_response(
|
||||
response,
|
||||
config.stream_decoder(),
|
||||
provider_name,
|
||||
let stream = match wire.body.get("stream") {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(Value::Bool(stream)) => *stream,
|
||||
Some(value) => {
|
||||
return Err(Error::InvalidRequest(
|
||||
litellm_llms::ErrorDetail::InvalidValue {
|
||||
field: "stream",
|
||||
expected: "a boolean",
|
||||
actual: value.clone(),
|
||||
},
|
||||
));
|
||||
}
|
||||
let text = response.text().await.map_err(network)?;
|
||||
log_response_body(&text);
|
||||
let raw = RawResponse { body: text.clone() };
|
||||
if let Some(observers) = observers {
|
||||
observers.emit(litellm_host::lifecycle::CallEvent::Execution(
|
||||
ExecutionEvent::ProviderResponseReceived { raw: raw.clone() },
|
||||
));
|
||||
}
|
||||
interceptors
|
||||
.after_provider_response(raw)
|
||||
.await
|
||||
.map_err(Error::post_call)?;
|
||||
decode_response(config, &body.model, &text)
|
||||
.map(|message| MessagesResponse::Complete(Box::new(message)))
|
||||
},
|
||||
)
|
||||
.await
|
||||
};
|
||||
Ok(ProviderCall {
|
||||
identity,
|
||||
wire,
|
||||
provider,
|
||||
signer: authenticated.signer,
|
||||
timeout,
|
||||
stream,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn call_provider(
|
||||
&self,
|
||||
request: ProviderCall,
|
||||
context: &CallContext<'_, impl Interceptors<Error>>,
|
||||
) -> Result<MessagesCallResponse, Error> {
|
||||
let ProviderCall {
|
||||
identity,
|
||||
wire,
|
||||
provider,
|
||||
signer,
|
||||
timeout,
|
||||
stream,
|
||||
} = request;
|
||||
let provider_name = provider.as_str();
|
||||
log_request_body(provider_name, stream, &wire.body);
|
||||
let response = send(
|
||||
&self.http,
|
||||
Authenticated {
|
||||
headers: wire.headers,
|
||||
signer,
|
||||
},
|
||||
&wire.url,
|
||||
&wire.body,
|
||||
timeout,
|
||||
)
|
||||
.await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(provider_error(response).await);
|
||||
}
|
||||
let config = provider.config();
|
||||
if stream {
|
||||
return Ok(streaming_response(
|
||||
response,
|
||||
config.stream_decoder(),
|
||||
provider_name,
|
||||
));
|
||||
}
|
||||
let text = response.text().await.map_err(network)?;
|
||||
log_response_body(&text);
|
||||
context.response_received(&text).await?;
|
||||
decode_response(config, &identity.model, &text)
|
||||
.map(|message| MessagesCallResponse::Complete(Box::new(message)))
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_failure(err: serde_json::Error) -> Error {
|
||||
|
|
@ -158,10 +184,10 @@ async fn provider_error(response: reqwest::Response) -> Error {
|
|||
}
|
||||
|
||||
fn decode_response(
|
||||
config: &dyn BaseAnthropicMessagesConfig,
|
||||
config: &dyn BaseMessagesConfig,
|
||||
model: &str,
|
||||
text: &str,
|
||||
) -> Result<AnthropicMessagesResponse, Error> {
|
||||
) -> Result<MessagesResponse, Error> {
|
||||
let response = serde_json::from_str(text).map_err(|err| {
|
||||
Error::InvalidResponse(litellm_llms::ErrorDetail::invalid(
|
||||
"messages response JSON",
|
||||
|
|
@ -177,7 +203,7 @@ fn streaming_response(
|
|||
response: reqwest::Response,
|
||||
decoder: Option<StreamDecoder>,
|
||||
provider: &'static str,
|
||||
) -> MessagesResponse {
|
||||
) -> MessagesCallResponse {
|
||||
let headers = response
|
||||
.headers()
|
||||
.iter()
|
||||
|
|
@ -194,7 +220,7 @@ fn streaming_response(
|
|||
.boxed(),
|
||||
Some(decode) => decoded_chunks(response, decode, provider),
|
||||
};
|
||||
MessagesResponse::Stream {
|
||||
MessagesCallResponse::Stream {
|
||||
head: super::route::MessagesStreamHead { headers },
|
||||
chunks,
|
||||
}
|
||||
|
|
@ -268,7 +294,7 @@ mod tests {
|
|||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let MessagesResponse::Stream { mut chunks, .. } =
|
||||
let MessagesCallResponse::Stream { mut chunks, .. } =
|
||||
streaming_response(response, Some(anthropic_sse_event_stream), "test")
|
||||
else {
|
||||
panic!("a streaming response returns chunks");
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
use litellm_host::observation::ObservationSender;
|
||||
mod common_utils;
|
||||
mod handler;
|
||||
mod prepare;
|
||||
pub mod route;
|
||||
mod types;
|
||||
|
||||
use futures_util::FutureExt;
|
||||
use litellm_auth::AuthServices;
|
||||
use litellm_host::interceptors::{ExecutionFacts, Interceptors, ResultSource};
|
||||
|
||||
use crate::{caching::CallCache, context::CallContext};
|
||||
use litellm_secrets::source::SecretSource;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use crate::error::RouteError as Error;
|
||||
pub use types::{MessagesCall, MessagesResponse, MessagesShaping, messages_body};
|
||||
pub use types::{MessagesCall, MessagesCallResponse, MessagesShaping, messages_body};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MessagesRoute {
|
||||
|
|
@ -20,74 +23,18 @@ pub struct MessagesRoute {
|
|||
cache: Option<litellm_cache_response::ScopedCache>,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MessagesRouteBuilder<Http = (), Auth = (), Secrets = ()> {
|
||||
http: Http,
|
||||
auth: Auth,
|
||||
secrets: Secrets,
|
||||
cache: Option<litellm_cache_response::ScopedCache>,
|
||||
}
|
||||
|
||||
impl<Http, Auth, Secrets> MessagesRouteBuilder<Http, Auth, Secrets> {
|
||||
pub fn with_http(
|
||||
self,
|
||||
http: litellm_http::Client,
|
||||
) -> MessagesRouteBuilder<litellm_http::Client, Auth, Secrets> {
|
||||
MessagesRouteBuilder {
|
||||
http,
|
||||
auth: self.auth,
|
||||
secrets: self.secrets,
|
||||
cache: self.cache,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_auth(
|
||||
self,
|
||||
auth: Arc<AuthServices>,
|
||||
) -> MessagesRouteBuilder<Http, Arc<AuthServices>, Secrets> {
|
||||
MessagesRouteBuilder {
|
||||
http: self.http,
|
||||
auth,
|
||||
secrets: self.secrets,
|
||||
cache: self.cache,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_secrets(
|
||||
self,
|
||||
secrets: Arc<dyn SecretSource>,
|
||||
) -> MessagesRouteBuilder<Http, Auth, Arc<dyn SecretSource>> {
|
||||
MessagesRouteBuilder {
|
||||
http: self.http,
|
||||
auth: self.auth,
|
||||
secrets,
|
||||
cache: self.cache,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_cache(self, cache: litellm_cache_response::ScopedCache) -> Self {
|
||||
Self {
|
||||
cache: Some(cache),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessagesRouteBuilder<litellm_http::Client, Arc<AuthServices>, Arc<dyn SecretSource>> {
|
||||
pub fn build(self) -> MessagesRoute {
|
||||
MessagesRoute {
|
||||
http: self.http,
|
||||
auth: self.auth,
|
||||
secrets: self.secrets,
|
||||
cache: self.cache,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessagesRoute {
|
||||
pub fn builder() -> MessagesRouteBuilder {
|
||||
MessagesRouteBuilder::default()
|
||||
pub fn new(
|
||||
http: litellm_http::Client,
|
||||
auth: Arc<AuthServices>,
|
||||
secrets: Arc<dyn SecretSource>,
|
||||
) -> Self {
|
||||
Self {
|
||||
http,
|
||||
auth,
|
||||
secrets,
|
||||
cache: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
|
@ -103,16 +50,10 @@ impl MessagesRoute {
|
|||
call: MessagesCall,
|
||||
interceptors: &impl litellm_host::interceptors::Interceptors<Error>,
|
||||
options: impl Into<crate::CallOptions>,
|
||||
) -> Result<MessagesResponse, Error> {
|
||||
let crate::CallOptions {
|
||||
cache: cache_options,
|
||||
observers,
|
||||
} = options.into();
|
||||
litellm_host::lifecycle::observe_call(
|
||||
observers.clone(),
|
||||
self.run(call, cache_options, interceptors, observers.as_ref()),
|
||||
)
|
||||
.await
|
||||
) -> Result<MessagesCallResponse, Error> {
|
||||
let context = CallContext::new(interceptors, options.into());
|
||||
litellm_host::lifecycle::observe_call(context.observers.clone(), self.run(call, context))
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "litellm.route", skip_all, fields(
|
||||
|
|
@ -126,36 +67,34 @@ impl MessagesRoute {
|
|||
async fn run(
|
||||
&self,
|
||||
call: MessagesCall,
|
||||
cache_options: Option<litellm_cache_response::CacheOptions>,
|
||||
interceptors: &impl litellm_host::interceptors::Interceptors<Error>,
|
||||
observers: Option<&ObservationSender>,
|
||||
) -> Result<MessagesResponse, Error> {
|
||||
context: CallContext<'_, impl Interceptors<Error>>,
|
||||
) -> Result<MessagesCallResponse, Error> {
|
||||
crate::diagnostic::call(async {
|
||||
self.run_provider(call, cache_options, interceptors, observers)
|
||||
.await
|
||||
let prepared = prepare::prepare(call, self.secrets.as_ref()).await?;
|
||||
crate::diagnostic::provider(&prepared.body.model, prepared.provider.as_str());
|
||||
let request = self.prepare_outbound(prepared, &context).boxed().await?;
|
||||
let cache = CallCache::<route::Messages>::from_wire(
|
||||
self.cache.as_ref().filter(|_| request.cacheable()),
|
||||
context.cache,
|
||||
&request.identity,
|
||||
&request.wire,
|
||||
);
|
||||
let identity = request.identity.clone();
|
||||
let (output, source) = match cache.lookup().await {
|
||||
Some(hit) => hit,
|
||||
None => (
|
||||
self.call_provider(request, &context).await?,
|
||||
ResultSource::Provider,
|
||||
),
|
||||
};
|
||||
context
|
||||
.result_ready(ExecutionFacts {
|
||||
provider: identity,
|
||||
source: source.clone(),
|
||||
})
|
||||
.await?;
|
||||
Ok(cache.finish(output, &source).await)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn run_provider(
|
||||
&self,
|
||||
call: MessagesCall,
|
||||
cache_options: Option<litellm_cache_response::CacheOptions>,
|
||||
interceptors: &impl litellm_host::interceptors::Interceptors<Error>,
|
||||
observers: Option<&ObservationSender>,
|
||||
) -> Result<MessagesResponse, Error> {
|
||||
let request = prepare::prepare(call, self.secrets.as_ref()).await?;
|
||||
crate::diagnostic::provider(&request.body.model, request.provider.as_str());
|
||||
let execute: futures_util::future::BoxFuture<'_, Result<MessagesResponse, Error>> =
|
||||
Box::pin(handler::execute(
|
||||
&self.http,
|
||||
&self.auth,
|
||||
request,
|
||||
self.cache.clone(),
|
||||
cache_options,
|
||||
interceptors,
|
||||
observers,
|
||||
));
|
||||
execute.await
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ use litellm_http::request::with_default_headers;
|
|||
use litellm_llms::base_llm::{
|
||||
auth::ValidatedEnvironment, messages::context::MessagesTransformContext,
|
||||
};
|
||||
use litellm_llms_types::formats::messages::MessagesRequest;
|
||||
use litellm_secrets::source::SecretSource;
|
||||
use litellm_types::llms::anthropic_messages::anthropic_request::AnthropicMessagesRequest;
|
||||
|
||||
use super::{
|
||||
Error, MessagesCall,
|
||||
|
|
@ -27,7 +27,7 @@ struct ResolvedProvider {
|
|||
pub(super) struct ProviderMessagesRequest {
|
||||
pub(super) provider: MessagesProvider,
|
||||
pub(super) url: String,
|
||||
pub(super) body: AnthropicMessagesRequest,
|
||||
pub(super) body: MessagesRequest,
|
||||
pub(super) environment: ValidatedEnvironment,
|
||||
pub(super) timeout: Option<Duration>,
|
||||
/// The caller's own credential, reported to the host beside the wire request.
|
||||
|
|
@ -79,7 +79,7 @@ fn prepare_provider_request(
|
|||
let env_lookup = |key: &str| secrets.get(key);
|
||||
|
||||
let sanitized = config.shape_request(
|
||||
AnthropicMessagesRequest { model, ..body },
|
||||
MessagesRequest { model, ..body },
|
||||
shaping.reasoning_auto_summary,
|
||||
)?;
|
||||
let trimmed = without_additional_drop_params(sanitized, &shaping.additional_drop_params)?;
|
||||
|
|
@ -124,9 +124,9 @@ fn prepare_provider_request(
|
|||
}
|
||||
|
||||
fn without_additional_drop_params(
|
||||
request: AnthropicMessagesRequest,
|
||||
request: MessagesRequest,
|
||||
paths: &[String],
|
||||
) -> Result<AnthropicMessagesRequest, Error> {
|
||||
) -> Result<MessagesRequest, Error> {
|
||||
if paths.is_empty() {
|
||||
return Ok(request);
|
||||
}
|
||||
|
|
@ -134,7 +134,7 @@ fn without_additional_drop_params(
|
|||
let trimmed = paths
|
||||
.iter()
|
||||
.fold(params, |params, path| delete_nested_value(params, path));
|
||||
Ok(AnthropicMessagesRequest {
|
||||
Ok(MessagesRequest {
|
||||
params: serde_json::from_value(trimmed).map_err(invalid_request)?,
|
||||
..request
|
||||
})
|
||||
|
|
@ -143,7 +143,7 @@ fn without_additional_drop_params(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use litellm_llms::base_llm::auth::resolve_auth;
|
||||
use litellm_types::utils::ProviderSpecificHeaders;
|
||||
use litellm_llms_types::headers::ProviderSpecificHeaders;
|
||||
use rstest::{fixture, rstest};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ mod tests {
|
|||
MessagesShaping::default()
|
||||
}
|
||||
|
||||
fn body(value: Value) -> AnthropicMessagesRequest {
|
||||
fn body(value: Value) -> MessagesRequest {
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ use litellm_host::{
|
|||
call::{HostedCompletion, HostedMachine, hosted_call},
|
||||
protocol::Protocol,
|
||||
};
|
||||
use litellm_types::llms::anthropic_messages::anthropic_response::AnthropicMessagesResponse;
|
||||
use litellm_llms_types::formats::messages::MessagesResponse;
|
||||
|
||||
use super::{Error, MessagesCall};
|
||||
|
||||
pub type MessagesOutput = HostedCompletion<Box<AnthropicMessagesResponse>>;
|
||||
pub type MessagesOutput = HostedCompletion<Box<MessagesResponse>>;
|
||||
|
||||
/// The upstream response as the caller sees it at stream hand-off, before any chunk.
|
||||
pub struct MessagesStreamHead {
|
||||
|
|
@ -19,7 +19,7 @@ pub struct MessagesStreamHead {
|
|||
pub struct Messages;
|
||||
|
||||
impl Protocol for Messages {
|
||||
type Response = Box<AnthropicMessagesResponse>;
|
||||
type Response = Box<MessagesResponse>;
|
||||
type Error = Error;
|
||||
type Request = MessagesCall;
|
||||
type HostCall = Infallible;
|
||||
|
|
@ -43,8 +43,14 @@ impl super::MessagesRoute {
|
|||
request,
|
||||
observers,
|
||||
move |call, _, interceptors, observers| async move {
|
||||
self.run(call, cache_options, &interceptors, observers.as_ref())
|
||||
.await
|
||||
let context = crate::context::CallContext::new(
|
||||
&interceptors,
|
||||
crate::CallOptions {
|
||||
cache: cache_options,
|
||||
observers,
|
||||
},
|
||||
);
|
||||
self.run(call, context).await
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@ use std::time::Duration;
|
|||
|
||||
use bytes::Bytes;
|
||||
use litellm_host::call::CallOutput;
|
||||
use litellm_llms::base_llm::messages::context::MessagesModelCapabilities as AnthropicModelCapabilities;
|
||||
use litellm_types::{
|
||||
llms::anthropic_messages::{
|
||||
anthropic_request::AnthropicMessagesRequest, anthropic_response::AnthropicMessagesResponse,
|
||||
},
|
||||
utils::ProviderSpecificHeaders,
|
||||
use litellm_llms::base_llm::messages::context::MessagesModelCapabilities;
|
||||
use litellm_llms_types::{
|
||||
formats::messages::{MessagesRequest, MessagesResponse},
|
||||
headers::ProviderSpecificHeaders,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
|
|
@ -15,7 +13,7 @@ use serde_json::{Map, Value};
|
|||
use super::Error;
|
||||
|
||||
pub struct MessagesCall {
|
||||
pub body: AnthropicMessagesRequest,
|
||||
pub body: MessagesRequest,
|
||||
pub api_key: Option<String>,
|
||||
pub api_base: Option<String>,
|
||||
pub custom_llm_provider: Option<String>,
|
||||
|
|
@ -25,7 +23,7 @@ pub struct MessagesCall {
|
|||
pub shaping: MessagesShaping,
|
||||
}
|
||||
|
||||
pub fn messages_body(body: Map<String, Value>) -> Result<AnthropicMessagesRequest, Error> {
|
||||
pub fn messages_body(body: Map<String, Value>) -> Result<MessagesRequest, Error> {
|
||||
serde_json::from_value(Value::Object(body)).map_err(invalid_request)
|
||||
}
|
||||
|
||||
|
|
@ -33,13 +31,13 @@ pub(super) fn invalid_request(err: serde_json::Error) -> Error {
|
|||
Error::InvalidRequest(format!("invalid Anthropic messages request: {err}").into())
|
||||
}
|
||||
|
||||
pub type MessagesResponse =
|
||||
CallOutput<Box<AnthropicMessagesResponse>, super::route::MessagesStreamHead, Bytes, Error>;
|
||||
pub type MessagesCallResponse =
|
||||
CallOutput<Box<MessagesResponse>, super::route::MessagesStreamHead, Bytes, Error>;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MessagesShaping {
|
||||
#[serde(default)]
|
||||
pub capabilities: AnthropicModelCapabilities,
|
||||
pub capabilities: MessagesModelCapabilities,
|
||||
#[serde(default)]
|
||||
pub drop_params: bool,
|
||||
#[serde(default)]
|
||||
|
|
@ -76,9 +74,9 @@ mod tests {
|
|||
#[case::partial_capabilities(
|
||||
json!({"capabilities": {"supports_reasoning": true}}),
|
||||
MessagesShaping {
|
||||
capabilities: AnthropicModelCapabilities {
|
||||
capabilities: MessagesModelCapabilities {
|
||||
supports_reasoning: true,
|
||||
..AnthropicModelCapabilities::default()
|
||||
..MessagesModelCapabilities::default()
|
||||
},
|
||||
..MessagesShaping::default()
|
||||
},
|
||||
|
|
@ -100,7 +98,7 @@ mod tests {
|
|||
"additional_drop_params": ["metadata.user_id", "thinking"]
|
||||
}),
|
||||
MessagesShaping {
|
||||
capabilities: AnthropicModelCapabilities {
|
||||
capabilities: MessagesModelCapabilities {
|
||||
supports_reasoning: true,
|
||||
supports_adaptive_thinking: true,
|
||||
thinking_always_on: false,
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ use litellm_host::observation::ObservationSender;
|
|||
use std::sync::Arc;
|
||||
|
||||
use litellm_host::interceptors::Interceptors;
|
||||
use litellm_llms::base_llm::ocr::{
|
||||
error::Error, handler::OcrClient, transformation::LiteLLMOcrResponse,
|
||||
};
|
||||
use litellm_llms::base_llm::ocr::{error::Error, handler::OcrClient};
|
||||
use litellm_llms_types::formats::ocr::LiteLLMOcrResponse;
|
||||
|
||||
use super::{
|
||||
handler::perform_ocr_request,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
use std::{collections::BTreeMap as Map, io::Read, path::Path};
|
||||
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use litellm_llms::base_llm::ocr::{
|
||||
error::Error,
|
||||
transformation::{OCR_INLINE_MAX_BYTES, OcrDocument},
|
||||
};
|
||||
use litellm_llms::base_llm::ocr::{error::Error, transformation::OCR_INLINE_MAX_BYTES};
|
||||
use litellm_llms_types::formats::ocr::OcrDocument;
|
||||
|
||||
use crate::ocr::types::OcrDocumentInput;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use futures_util::future::BoxFuture;
|
||||
use litellm_host::interceptors::{Interceptors, RawResponse, RequestContext, WireRequest};
|
||||
use litellm_host::lifecycle::ExecutionEvent;
|
||||
use litellm_host::observation::ObservationSender;
|
||||
use litellm_host::{lifecycle::ExecutionEvent, observation::ObservationSender};
|
||||
use litellm_llms::base_llm::ocr::{
|
||||
error::Error,
|
||||
handler::{CallHooks, OcrClient},
|
||||
transformation::{LiteLLMOcrResponse, PreparedOcrRequest},
|
||||
transformation::PreparedOcrRequest,
|
||||
};
|
||||
use litellm_llms_types::formats::ocr::LiteLLMOcrResponse;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{arguments::is_secret_param, prepare::prepare_request, provider_config::OcrConfigKind};
|
||||
|
|
|
|||
|
|
@ -85,12 +85,13 @@ mod tests {
|
|||
base_llm::ocr::{
|
||||
error::Error,
|
||||
handler::{CallHooks, OcrClient},
|
||||
transformation::{BaseOcrConfig, OcrResponseFormat},
|
||||
transformation::BaseOcrConfig,
|
||||
},
|
||||
cohere::ocr::transformation::CohereParseConfig,
|
||||
mistral::ocr::transformation::MistralOcrConfig,
|
||||
vertex_ai::ocr::transformation::VertexAiOcrConfig,
|
||||
};
|
||||
use litellm_llms_types::formats::ocr::OcrResponseFormat;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ use litellm_llms::{
|
|||
error::Error,
|
||||
handler::{self, CallHooks, OcrClient},
|
||||
transformation::{
|
||||
BaseOcrConfig, LiteLLMOcrResponse, OcrCredentialInputs, OcrDocument, OcrResponseFormat,
|
||||
PreparedOcrRequest, ResolvedOcrCredentials,
|
||||
BaseOcrConfig, OcrCredentialInputs, PreparedOcrRequest, ResolvedOcrCredentials,
|
||||
},
|
||||
},
|
||||
cohere::ocr::transformation::CohereParseConfig,
|
||||
|
|
@ -25,6 +24,7 @@ use litellm_llms::{
|
|||
deepseek_transformation::VertexAIDeepSeekOCRConfig, transformation::VertexAiOcrConfig,
|
||||
},
|
||||
};
|
||||
use litellm_llms_types::formats::ocr::{LiteLLMOcrResponse, OcrDocument, OcrResponseFormat};
|
||||
|
||||
macro_rules! with_config {
|
||||
($kind:expr, $config:ident => $body:expr) => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ use litellm_host::{
|
|||
protocol::Protocol,
|
||||
protocol::Reply,
|
||||
};
|
||||
use litellm_llms::base_llm::ocr::{error::Error, transformation::LiteLLMOcrResponse};
|
||||
use litellm_llms::base_llm::ocr::error::Error;
|
||||
use litellm_llms_types::formats::ocr::LiteLLMOcrResponse;
|
||||
|
||||
use crate::ocr::types::{LiteLLMOcrRequest, OcrDocumentInput};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ use litellm_auth::{InputSource, SecretValue, TokenProviderHandle};
|
|||
use litellm_core_utils::call_arguments::CallArguments;
|
||||
use litellm_llms::base_llm::ocr::{
|
||||
error::Error,
|
||||
transformation::{
|
||||
OcrCredentialInputs, OcrDocument, OcrResponseFormat, OcrTransportConfig, response_format,
|
||||
},
|
||||
transformation::{OcrCredentialInputs, OcrTransportConfig, response_format},
|
||||
};
|
||||
use litellm_llms_types::formats::ocr::{OcrDocument, OcrResponseFormat};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use super::provider_config::{OcrConfigKind, resolve_provider_config};
|
||||
|
|
@ -222,7 +221,7 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
fn document() -> OcrDocument {
|
||||
OcrDocument::try_from(
|
||||
serde_json::from_value(
|
||||
json!({"type":"document_url","document_url":"data:application/pdf;base64,YWJj"}),
|
||||
)
|
||||
.unwrap()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
use std::{collections::BTreeMap, time::Duration};
|
||||
|
||||
use litellm_auth::{InputSource, SecretValue};
|
||||
use litellm_llms::base_llm::ocr::{
|
||||
error::Error,
|
||||
transformation::{OcrDocument, decode_request_value},
|
||||
};
|
||||
use litellm_llms::base_llm::ocr::{error::Error, transformation::decode_request_value};
|
||||
use litellm_llms_types::formats::ocr::OcrDocument;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ pub(super) async fn execute(
|
|||
auth: &litellm_auth::AuthServices,
|
||||
request: ProviderResponsesRequest,
|
||||
cache: Option<litellm_cache_response::ScopedCache>,
|
||||
cache_options: Option<litellm_cache_response::CacheOptions>,
|
||||
cache_options: Option<litellm_cache_response::CachePolicy>,
|
||||
interceptors: &impl Interceptors<Error>,
|
||||
observers: Option<&ObservationSender>,
|
||||
) -> Result<ResponsesOutput, Error> {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ impl ResponsesRoute {
|
|||
async fn run(
|
||||
&self,
|
||||
call: ResponsesCall,
|
||||
cache_options: Option<litellm_cache_response::CacheOptions>,
|
||||
cache_options: Option<litellm_cache_response::CachePolicy>,
|
||||
interceptors: &impl litellm_host::interceptors::Interceptors<Error>,
|
||||
observers: Option<&ObservationSender>,
|
||||
) -> Result<ResponsesOutput, Error> {
|
||||
|
|
@ -85,7 +85,7 @@ impl ResponsesRoute {
|
|||
async fn run_provider(
|
||||
&self,
|
||||
call: ResponsesCall,
|
||||
cache_options: Option<litellm_cache_response::CacheOptions>,
|
||||
cache_options: Option<litellm_cache_response::CachePolicy>,
|
||||
interceptors: &impl Interceptors<Error>,
|
||||
observers: Option<&ObservationSender>,
|
||||
) -> Result<ResponsesOutput, Error> {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use litellm_host::{
|
|||
call::{HostedMachine, hosted_call},
|
||||
protocol::Protocol,
|
||||
};
|
||||
use litellm_types::responses::main::ResponsesApiResponse;
|
||||
use litellm_llms_types::formats::responses::ResponsesApiResponse;
|
||||
|
||||
use super::{
|
||||
Error, ResponsesRoute,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use litellm_host::call::CallOutput;
|
|||
use litellm_llms::base_llm::{
|
||||
auth::ValidatedEnvironment, responses::transformation::BaseResponsesApiConfig,
|
||||
};
|
||||
use litellm_types::responses::main::ResponsesApiResponse;
|
||||
use litellm_llms_types::formats::responses::ResponsesApiResponse;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use super::Error;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc, time::Duration};
|
|||
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use litellm_http::websocket::{UpstreamWebSocket, connect_upstream};
|
||||
use litellm_types::responses::streaming_websocket::ResponsesWsEventType;
|
||||
use litellm_llms_types::formats::responses::streaming_websocket::ResponsesWsEventType;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_tungstenite::tungstenite::{
|
||||
Message,
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ use bytes::Bytes;
|
|||
use futures_util::{StreamExt, TryStreamExt, stream};
|
||||
use litellm_cache_memory::InMemoryCache;
|
||||
use litellm_cache_response::{
|
||||
CacheOptions, CacheScope, ResponseCache, ResponseCacheConfig, ResponseCacheService,
|
||||
ResponseEnvelope,
|
||||
CacheOptions, CachePolicy, CacheScope, ResponseCache, ResponseCacheConfig,
|
||||
ResponseCacheService, ResponseEnvelope,
|
||||
};
|
||||
use litellm_core::{
|
||||
RouteError,
|
||||
|
|
@ -117,9 +117,9 @@ async fn call(
|
|||
|
||||
#[rstest]
|
||||
#[case::normal(CacheOptions::new(CacheScope::Shared), true, true)]
|
||||
#[case::no_cache(CacheOptions { no_cache: true, ..CacheOptions::new(CacheScope::Shared) }, false, true)]
|
||||
#[case::no_store(CacheOptions { no_store: true, ..CacheOptions::new(CacheScope::Shared) }, true, false)]
|
||||
#[case::disabled(CacheOptions { caching: Some(false), ..CacheOptions::new(CacheScope::Shared) }, false, false)]
|
||||
#[case::no_cache(CacheOptions { policy: CachePolicy { no_cache: true, ..CachePolicy::default() }, ..CacheOptions::new(CacheScope::Shared) }, false, true)]
|
||||
#[case::no_store(CacheOptions { policy: CachePolicy { no_store: true, ..CachePolicy::default() }, ..CacheOptions::new(CacheScope::Shared) }, true, false)]
|
||||
#[case::disabled(CacheOptions { policy: CachePolicy { caching: Some(false), ..CachePolicy::default() }, ..CacheOptions::new(CacheScope::Shared) }, false, false)]
|
||||
#[tokio::test]
|
||||
async fn cache_controls_apply_to_both_reads_and_writes(
|
||||
cache: Arc<dyn ResponseCacheService>,
|
||||
|
|
@ -411,7 +411,7 @@ async fn responses_refetches_instead_of_deserializing_another_api_response(
|
|||
#[case] poisoned: Value,
|
||||
) {
|
||||
use litellm_core::responses::route::Responses;
|
||||
use litellm_types::responses::main::ResponsesApiResponse;
|
||||
use litellm_llms_types::formats::responses::ResponsesApiResponse;
|
||||
|
||||
let cache: Arc<dyn ResponseCacheService> = Arc::new(InvalidEntryCache(
|
||||
ResponseCache::new(Arc::new(InMemoryCache::default())),
|
||||
|
|
@ -461,7 +461,7 @@ async fn messages_cache_identity_includes_provider_native_parameters(
|
|||
#[case] changed: Value,
|
||||
) {
|
||||
use litellm_core::messages::route::Messages;
|
||||
use litellm_types::llms::anthropic_messages::anthropic_response::AnthropicMessagesResponse;
|
||||
use litellm_llms_types::formats::messages::MessagesResponse;
|
||||
|
||||
let calls = AtomicUsize::new(0);
|
||||
for (value, expected_call) in [(original.clone(), 0), (changed, 1), (original, 0)] {
|
||||
|
|
@ -487,7 +487,7 @@ async fn messages_cache_identity_includes_provider_native_parameters(
|
|||
None,
|
||||
|| async {
|
||||
let call = calls.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(Box::new(serde_json::from_value::<AnthropicMessagesResponse>(json!({
|
||||
Ok(Box::new(serde_json::from_value::<MessagesResponse>(json!({
|
||||
"id":call.to_string(), "type":"message", "role":"assistant", "model":"test",
|
||||
"content":[{"type":"text","text":format!("answer {call}")}],
|
||||
"stop_reason":"end_turn", "stop_sequence":null
|
||||
|
|
@ -723,9 +723,9 @@ async fn unary_call(
|
|||
|
||||
#[rstest]
|
||||
#[case::normal(CacheOptions::new(CacheScope::Shared), true, true)]
|
||||
#[case::no_cache(CacheOptions { no_cache: true, ..CacheOptions::new(CacheScope::Shared) }, false, true)]
|
||||
#[case::no_store(CacheOptions { no_store: true, ..CacheOptions::new(CacheScope::Shared) }, true, false)]
|
||||
#[case::disabled(CacheOptions { caching: Some(false), ..CacheOptions::new(CacheScope::Shared) }, false, false)]
|
||||
#[case::no_cache(CacheOptions { policy: CachePolicy { no_cache: true, ..CachePolicy::default() }, ..CacheOptions::new(CacheScope::Shared) }, false, true)]
|
||||
#[case::no_store(CacheOptions { policy: CachePolicy { no_store: true, ..CachePolicy::default() }, ..CacheOptions::new(CacheScope::Shared) }, true, false)]
|
||||
#[case::disabled(CacheOptions { policy: CachePolicy { caching: Some(false), ..CachePolicy::default() }, ..CacheOptions::new(CacheScope::Shared) }, false, false)]
|
||||
#[tokio::test]
|
||||
async fn unary_cache_controls_do_not_change_the_shared_service(
|
||||
cache: Arc<dyn ResponseCacheService>,
|
||||
|
|
@ -843,7 +843,7 @@ async fn responses_cache_only_reuses_completed_responses(
|
|||
#[case] expected_calls: usize,
|
||||
) {
|
||||
use litellm_core::responses::route::Responses;
|
||||
use litellm_types::responses::main::ResponsesApiResponse;
|
||||
use litellm_llms_types::formats::responses::ResponsesApiResponse;
|
||||
|
||||
let calls = AtomicUsize::new(0);
|
||||
for _ in 0..2 {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use std::time::Duration;
|
|||
|
||||
use litellm_core::chat_completions::{Error, types::ChatCompletionsRequest};
|
||||
use litellm_http::transport::Error as TransportError;
|
||||
use litellm_types::utils::ChatCompletionsResponse;
|
||||
use litellm_llms_types::formats::chat_completions::ChatCompletionsResponse;
|
||||
use rstest::{fixture, rstest};
|
||||
use serde_json::{Map, Value, json};
|
||||
use wiremock::ResponseTemplate;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use litellm_host::lifecycle::ExecutionEvent;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use litellm_core::messages::route::Messages;
|
||||
use litellm_core::messages::{MessagesCallResponse, route::Messages};
|
||||
use litellm_host::{
|
||||
interceptors::{RequestContext, WireRequest},
|
||||
interceptors::{ExecutionFacts, RequestContext, ResultSource, WireRequest},
|
||||
lifecycle::CallEvent,
|
||||
};
|
||||
use litellm_llms::base_llm::messages::context::MessagesModelCapabilities as AnthropicModelCapabilities;
|
||||
|
|
@ -20,6 +20,8 @@ struct RecordingHost {
|
|||
rewrite: Rewrite,
|
||||
events: super::support::Observations,
|
||||
optional_params: Mutex<Vec<Value>>,
|
||||
facts: Mutex<Vec<ExecutionFacts>>,
|
||||
reject_result: bool,
|
||||
}
|
||||
|
||||
impl RecordingHost {
|
||||
|
|
@ -29,6 +31,8 @@ impl RecordingHost {
|
|||
rewrite,
|
||||
events: super::support::Observations::default(),
|
||||
optional_params: Mutex::new(Vec::new()),
|
||||
facts: Mutex::new(Vec::new()),
|
||||
reject_result: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +77,14 @@ impl litellm_host::lifecycle::CallObserver for RecordingHost {
|
|||
impl litellm_host::interceptors::Interceptors<<Messages as litellm_host::protocol::Protocol>::Error>
|
||||
for RecordingHost
|
||||
{
|
||||
async fn result_ready(&self, facts: ExecutionFacts) -> Result<(), Error> {
|
||||
self.facts.lock().unwrap().push(facts);
|
||||
if self.reject_result {
|
||||
return Err(Error::Unsupported("result rejected"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn before_provider_request(
|
||||
&self,
|
||||
wire: WireRequest,
|
||||
|
|
@ -98,6 +110,91 @@ impl litellm_host::interceptors::Interceptors<<Messages as litellm_host::protoco
|
|||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::native_unary(false, false)]
|
||||
#[case::native_stream(true, false)]
|
||||
#[case::hosted_unary(false, true)]
|
||||
#[case::hosted_stream(true, true)]
|
||||
#[tokio::test]
|
||||
async fn rejected_results_are_not_delivered_or_cached(
|
||||
call: MessagesCall,
|
||||
#[case] streaming: bool,
|
||||
#[case] hosted: bool,
|
||||
) {
|
||||
use futures_util::TryStreamExt;
|
||||
use litellm_cache_memory::InMemoryCache;
|
||||
use litellm_cache_response::{CacheScope, ResponseCache, ScopedCache};
|
||||
|
||||
let response = if streaming {
|
||||
ResponseTemplate::new(200).set_body_raw(
|
||||
"event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
|
||||
"text/event-stream",
|
||||
)
|
||||
} else {
|
||||
message_response()
|
||||
};
|
||||
let upstream = upstream([response.clone(), response]).await;
|
||||
let route = messages_route(no_secrets()).with_cache(ScopedCache::new(
|
||||
Arc::new(ResponseCache::new(Arc::new(InMemoryCache::new(
|
||||
Some(100),
|
||||
Some(Duration::from_secs(60)),
|
||||
)))),
|
||||
CacheScope::Shared,
|
||||
));
|
||||
for (reject, expected_requests, cached) in [
|
||||
(true, 1, false),
|
||||
(false, 2, false),
|
||||
(true, 2, true),
|
||||
(false, 2, true),
|
||||
] {
|
||||
let request = authenticated(
|
||||
with_fields(
|
||||
MessagesCall {
|
||||
body: call.body.clone(),
|
||||
..super::call()
|
||||
},
|
||||
json!({"stream": streaming}),
|
||||
),
|
||||
upstream.uri(),
|
||||
);
|
||||
let host = RecordingHost {
|
||||
reject_result: reject,
|
||||
..RecordingHost::passthrough(request)
|
||||
};
|
||||
let result = if hosted {
|
||||
litellm_host_native::in_process::run_hosted(
|
||||
route.clone().machine(host.request().unwrap(), None),
|
||||
host.runtime(),
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
} else {
|
||||
match route.execute(host.request().unwrap(), &host, None).await {
|
||||
Ok(MessagesCallResponse::Complete(_)) => Ok(()),
|
||||
Ok(MessagesCallResponse::Stream { chunks, .. }) => {
|
||||
chunks.try_collect::<Vec<_>>().await.map(|_| ())
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
};
|
||||
assert_eq!(
|
||||
result,
|
||||
if reject {
|
||||
Err(Error::Unsupported("result rejected"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
);
|
||||
assert_eq!(received(&upstream).await.len(), expected_requests);
|
||||
let facts = host.facts.lock().unwrap();
|
||||
assert_eq!(facts.len(), 1);
|
||||
assert_eq!(
|
||||
matches!(facts[0].source, ResultSource::Cache { .. }),
|
||||
cached
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_through(host: &RecordingHost) -> Result<MessagesOutput, Error> {
|
||||
litellm_host_native::in_process::run_hosted(
|
||||
machine(Arc::new(RecordingSecrets::empty()))(host.request()?),
|
||||
|
|
@ -143,6 +240,81 @@ async fn what_before_send_returns_is_what_the_provider_receives(call: MessagesCa
|
|||
assert_eq!(request.header("x-api-key"), Some("sk-ant"));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::enable(false, json!(true), Some(true))]
|
||||
#[case::disable(true, json!(false), Some(false))]
|
||||
#[case::null(true, Value::Null, Some(false))]
|
||||
#[case::invalid(false, json!("true"), None)]
|
||||
#[tokio::test]
|
||||
async fn response_mode_follows_the_intercepted_request(
|
||||
call: MessagesCall,
|
||||
traces: TraceCapture,
|
||||
#[case] original_stream: bool,
|
||||
#[case] rewritten_stream: Value,
|
||||
#[case] expected_stream: Option<bool>,
|
||||
) {
|
||||
use futures_util::TryStreamExt;
|
||||
|
||||
let sse = "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n";
|
||||
let response = if expected_stream == Some(true) {
|
||||
ResponseTemplate::new(200).set_body_raw(sse, "text/event-stream")
|
||||
} else {
|
||||
message_response()
|
||||
};
|
||||
let upstream = upstream([response]).await;
|
||||
let rewrite = rewritten_stream.clone();
|
||||
let host = RecordingHost::new(
|
||||
authenticated(
|
||||
with_fields(call, json!({"stream": original_stream})),
|
||||
upstream.uri(),
|
||||
),
|
||||
Box::new(move |wire| {
|
||||
let mut body = wire.body;
|
||||
body["stream"] = rewrite.clone();
|
||||
Ok(WireRequest { body, ..wire })
|
||||
}),
|
||||
);
|
||||
let result = traces
|
||||
.logger()
|
||||
.instrument(async {
|
||||
let output = messages_route(no_secrets())
|
||||
.execute(host.request()?, &host, None)
|
||||
.await?;
|
||||
match output {
|
||||
MessagesCallResponse::Stream { chunks, .. } => {
|
||||
assert_eq!(expected_stream, Some(true));
|
||||
assert_eq!(
|
||||
chunks.try_collect::<Vec<_>>().await?.concat(),
|
||||
sse.as_bytes()
|
||||
);
|
||||
}
|
||||
MessagesCallResponse::Complete(message) => {
|
||||
assert_eq!(expected_stream, Some(false));
|
||||
assert_eq!(*message, serde_json::from_value(message_body()).unwrap());
|
||||
}
|
||||
}
|
||||
Ok::<_, Error>(())
|
||||
})
|
||||
.await;
|
||||
|
||||
let summaries = traces.summaries("litellm.route");
|
||||
assert_eq!(summaries.len(), 1);
|
||||
let Some(expected_stream) = expected_stream else {
|
||||
assert!(matches!(result, Err(Error::InvalidRequest(_))));
|
||||
assert!(received(&upstream).await.is_empty());
|
||||
assert_eq!(summaries[0]["outcome"], "failure");
|
||||
return;
|
||||
};
|
||||
result.unwrap();
|
||||
assert_eq!(
|
||||
only_request(&upstream).await.json()["stream"],
|
||||
rewritten_stream
|
||||
);
|
||||
assert_eq!(host.raw_responses().len(), usize::from(!expected_stream));
|
||||
assert_eq!(summaries[0]["stream"], expected_stream);
|
||||
assert_eq!(summaries[0]["outcome"], "success");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn a_before_send_failure_never_sends(call: MessagesCall) {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,8 @@ use litellm_core::messages::{
|
|||
route::{Messages, MessagesMachine, MessagesOutput},
|
||||
};
|
||||
use litellm_http::{HttpSettings, Resolution};
|
||||
use litellm_llms_types::formats::messages::{MessagesRequest, MessagesResponse};
|
||||
use litellm_secrets::source::SecretSource;
|
||||
use litellm_types::llms::anthropic_messages::{
|
||||
anthropic_request::AnthropicMessagesRequest, anthropic_response::AnthropicMessagesResponse,
|
||||
};
|
||||
use rstest::fixture;
|
||||
use serde_json::{Map, Value, json};
|
||||
use wiremock::ResponseTemplate;
|
||||
|
|
@ -35,7 +33,7 @@ fn object(value: Value) -> Map<String, Value> {
|
|||
map
|
||||
}
|
||||
|
||||
fn body(value: Value) -> AnthropicMessagesRequest {
|
||||
fn body(value: Value) -> MessagesRequest {
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +114,7 @@ async fn run(call: MessagesCall) -> Result<MessagesOutput, Error> {
|
|||
run_with(Arc::new(RecordingSecrets::empty()), call).await
|
||||
}
|
||||
|
||||
async fn run_message(call: MessagesCall) -> AnthropicMessagesResponse {
|
||||
async fn run_message(call: MessagesCall) -> MessagesResponse {
|
||||
match run(call).await.expect("messages call succeeds") {
|
||||
MessagesOutput::Complete(message) => *message,
|
||||
MessagesOutput::StreamEnded | MessagesOutput::Detached => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use litellm_llms::base_llm::messages::context::{MessagesModelCapabilities, SupportedEffortTiers};
|
||||
use litellm_types::llms::anthropic::{AnthropicBeta, BetaSet};
|
||||
use litellm_types::utils::{ProviderSpecificHeader, ProviderSpecificHeaders};
|
||||
use litellm_llms_types::{
|
||||
headers::{ProviderSpecificHeader, ProviderSpecificHeaders},
|
||||
providers::anthropic::{AnthropicBeta, BetaSet},
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use litellm_core::messages::{MessagesResponse, messages_body};
|
||||
use litellm_core::messages::{MessagesCallResponse, messages_body};
|
||||
use litellm_host::{
|
||||
interceptors::{ExecutionFacts, ResultSource},
|
||||
lifecycle::ExecutionEvent,
|
||||
|
|
@ -33,7 +33,7 @@ async fn calls_defer_execution_until_polled(
|
|||
let request = host.request().unwrap();
|
||||
let observer: Option<litellm_host::observation::ObservationSender> =
|
||||
with_observer.then(|| host.events.0.sender.clone());
|
||||
let future: BoxFuture<'_, Result<MessagesResponse, Error>> = if with_hooks {
|
||||
let future: BoxFuture<'_, Result<MessagesCallResponse, Error>> = if with_hooks {
|
||||
Box::pin(route.execute(request, &host, observer))
|
||||
} else {
|
||||
Box::pin(route.execute(request, &(), observer))
|
||||
|
|
@ -43,7 +43,7 @@ async fn calls_defer_execution_until_polled(
|
|||
assert!(host.events.0.lock().unwrap().is_empty());
|
||||
assert!(received(&upstream).await.is_empty());
|
||||
|
||||
let MessagesResponse::Complete(response) = future.await.unwrap() else {
|
||||
let MessagesCallResponse::Complete(response) = future.await.unwrap() else {
|
||||
panic!("expected a completed message");
|
||||
};
|
||||
assert_eq!(
|
||||
|
|
@ -269,27 +269,24 @@ async fn the_facade_sends_through_the_injected_http_pool_configuration(call: Mes
|
|||
};
|
||||
|
||||
let resources = support::resources();
|
||||
let response = litellm_core::messages::MessagesRoute::builder()
|
||||
.with_http(provider_http(
|
||||
&resources,
|
||||
&Resolution::from(&settings).config,
|
||||
))
|
||||
.with_auth(resources.auth)
|
||||
.with_secrets(no_secrets())
|
||||
.build()
|
||||
.execute(
|
||||
MessagesCall {
|
||||
api_key: Some("sk-ant".into()),
|
||||
api_base: Some(base),
|
||||
..call
|
||||
},
|
||||
&(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("messages request succeeds");
|
||||
let response = litellm_core::messages::MessagesRoute::new(
|
||||
provider_http(&resources, &Resolution::from(&settings).config),
|
||||
resources.auth,
|
||||
no_secrets(),
|
||||
)
|
||||
.execute(
|
||||
MessagesCall {
|
||||
api_key: Some("sk-ant".into()),
|
||||
api_base: Some(base),
|
||||
..call
|
||||
},
|
||||
&(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("messages request succeeds");
|
||||
|
||||
let MessagesResponse::Complete(message) = response else {
|
||||
let MessagesCallResponse::Complete(message) = response else {
|
||||
panic!("a non-streaming request returns a message");
|
||||
};
|
||||
assert_eq!(message.id, "msg_1");
|
||||
|
|
@ -345,7 +342,7 @@ async fn message_route_summary_excludes_payload_diagnostics(
|
|||
#[case::uncached(false, 2)]
|
||||
#[case::cached(true, 1)]
|
||||
#[tokio::test]
|
||||
async fn builder_preserves_dependencies_and_optional_cache(
|
||||
async fn route_uses_injected_dependencies_and_optional_cache(
|
||||
#[case] caching: bool,
|
||||
#[case] expected_requests: usize,
|
||||
) {
|
||||
|
|
@ -355,9 +352,13 @@ async fn builder_preserves_dependencies_and_optional_cache(
|
|||
|
||||
let upstream = upstream([message_response(), message_response()]).await;
|
||||
let resources = resources();
|
||||
let builder = MessagesRoute::builder();
|
||||
let builder = if caching {
|
||||
builder.with_cache(ScopedCache::new(
|
||||
let route = MessagesRoute::new(
|
||||
provider_http(&resources, &http_config()),
|
||||
resources.auth.clone(),
|
||||
Arc::new(RecordingSecrets::new([("ANTHROPIC_API_KEY", "route-key")])),
|
||||
);
|
||||
let route = if caching {
|
||||
route.with_cache(ScopedCache::new(
|
||||
Arc::new(ResponseCache::new(Arc::new(InMemoryCache::new(
|
||||
Some(100),
|
||||
Some(Duration::from_secs(60)),
|
||||
|
|
@ -365,22 +366,15 @@ async fn builder_preserves_dependencies_and_optional_cache(
|
|||
CacheScope::Shared,
|
||||
))
|
||||
} else {
|
||||
builder
|
||||
route
|
||||
};
|
||||
let route = builder
|
||||
.with_secrets(Arc::new(RecordingSecrets::new([(
|
||||
"ANTHROPIC_API_KEY",
|
||||
"builder-key",
|
||||
)])))
|
||||
.with_auth(resources.auth.clone())
|
||||
.with_http(provider_http(&resources, &http_config()))
|
||||
.build();
|
||||
for _ in 0..2 {
|
||||
let request = MessagesCall {
|
||||
api_base: Some(upstream.uri()),
|
||||
..super::call()
|
||||
};
|
||||
let MessagesResponse::Complete(response) = route.execute(request, &(), None).await.unwrap()
|
||||
let MessagesCallResponse::Complete(response) =
|
||||
route.execute(request, &(), None).await.unwrap()
|
||||
else {
|
||||
panic!("expected a completed message");
|
||||
};
|
||||
|
|
@ -391,5 +385,72 @@ async fn builder_preserves_dependencies_and_optional_cache(
|
|||
}
|
||||
let requests = received(&upstream).await;
|
||||
assert_eq!(requests.len(), expected_requests);
|
||||
assert_eq!(requests[0].header("x-api-key"), Some("builder-key"));
|
||||
assert_eq!(requests[0].header("x-api-key"), Some("route-key"));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn cache_overrides_preserve_the_routes_isolated_scope(call: MessagesCall) {
|
||||
use litellm_cache_memory::InMemoryCache;
|
||||
use litellm_cache_response::{CachePolicy, CacheScope, ResponseCache, ScopedCache};
|
||||
|
||||
let first_body = message_body();
|
||||
let second_body = Value::Object(
|
||||
first_body
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
(
|
||||
key.clone(),
|
||||
if key == "id" {
|
||||
json!("msg_second")
|
||||
} else {
|
||||
value.clone()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
let upstream = upstream([
|
||||
json_response(first_body.clone()),
|
||||
json_response(second_body.clone()),
|
||||
])
|
||||
.await;
|
||||
let service = Arc::new(ResponseCache::new(Arc::new(InMemoryCache::new(
|
||||
Some(100),
|
||||
Some(Duration::from_secs(60)),
|
||||
))));
|
||||
let first = messages_route(no_secrets()).with_cache(ScopedCache::new(
|
||||
service.clone(),
|
||||
CacheScope::Isolated("first".into()),
|
||||
));
|
||||
let second = messages_route(no_secrets()).with_cache(ScopedCache::new(
|
||||
service,
|
||||
CacheScope::Isolated("second".into()),
|
||||
));
|
||||
for (route, expected) in [
|
||||
(&first, &first_body),
|
||||
(&second, &second_body),
|
||||
(&first, &first_body),
|
||||
(&second, &second_body),
|
||||
] {
|
||||
let request = MessagesCall {
|
||||
body: call.body.clone(),
|
||||
api_key: Some("same-key".into()),
|
||||
api_base: Some(upstream.uri()),
|
||||
..super::call()
|
||||
};
|
||||
let override_options = CachePolicy {
|
||||
ttl: Some(Duration::from_secs(30)),
|
||||
..CachePolicy::default()
|
||||
};
|
||||
let MessagesCallResponse::Complete(response) =
|
||||
route.execute(request, &(), override_options).await.unwrap()
|
||||
else {
|
||||
panic!("expected a completed message");
|
||||
};
|
||||
assert_eq!(response.id, expected["id"].as_str().unwrap());
|
||||
}
|
||||
assert_eq!(received(&upstream).await.len(), 2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
use bytes::Bytes;
|
||||
use futures_util::{StreamExt, TryStreamExt};
|
||||
use litellm_core::messages::{
|
||||
MessagesResponse,
|
||||
MessagesCallResponse,
|
||||
route::{Messages, MessagesStreamHead},
|
||||
};
|
||||
use litellm_tracing::{Logger, Metadata, Record, Sink};
|
||||
|
|
@ -353,7 +353,7 @@ async fn the_sdk_returns_stream_headers_and_every_sse_byte(
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let MessagesResponse::Stream { head, chunks } = response else {
|
||||
let MessagesCallResponse::Stream { head, chunks } = response else {
|
||||
panic!("a streaming request returns a stream");
|
||||
};
|
||||
for (name, value) in UPSTREAM_HEADERS {
|
||||
|
|
@ -407,7 +407,7 @@ async fn dropping_the_sdk_stream_closes_the_unfinished_upstream(
|
|||
.expect("messages() returns before the upstream finishes")
|
||||
.unwrap();
|
||||
|
||||
let MessagesResponse::Stream { mut chunks, .. } = response else {
|
||||
let MessagesCallResponse::Stream { mut chunks, .. } = response else {
|
||||
panic!("a streaming request returns a stream");
|
||||
};
|
||||
if read_chunk {
|
||||
|
|
@ -442,7 +442,7 @@ async fn the_sdk_yields_a_body_error_once_after_delivered_chunks(call: MessagesC
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let MessagesResponse::Stream { mut chunks, .. } = response else {
|
||||
let MessagesCallResponse::Stream { mut chunks, .. } = response else {
|
||||
panic!("a streaming request returns a stream");
|
||||
};
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -9,11 +9,8 @@ use litellm_host::{
|
|||
interceptors::{RequestContext, WireRequest},
|
||||
lifecycle::CallEvent,
|
||||
};
|
||||
use litellm_llms::base_llm::ocr::{
|
||||
error::Error,
|
||||
settings::OcrSettings,
|
||||
transformation::{LiteLLMOcrResponse, OcrDocument},
|
||||
};
|
||||
use litellm_llms::base_llm::ocr::{error::Error, settings::OcrSettings};
|
||||
use litellm_llms_types::formats::ocr::{LiteLLMOcrResponse, OcrDocument};
|
||||
use serde_json::{Map, Value, json};
|
||||
use std::sync::Mutex;
|
||||
use wiremock::{MockServer, ResponseTemplate};
|
||||
|
|
|
|||
|
|
@ -44,11 +44,11 @@ pub fn provider_http(
|
|||
|
||||
pub fn messages_route(secrets: Arc<dyn SecretSource>) -> litellm_core::messages::MessagesRoute {
|
||||
let resources = resources();
|
||||
litellm_core::messages::MessagesRoute::builder()
|
||||
.with_http(provider_http(&resources, &http_config()))
|
||||
.with_auth(resources.auth)
|
||||
.with_secrets(secrets)
|
||||
.build()
|
||||
litellm_core::messages::MessagesRoute::new(
|
||||
provider_http(&resources, &http_config()),
|
||||
resources.auth,
|
||||
secrets,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn chat_completions_route() -> litellm_core::chat_completions::ChatCompletionsRoute {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ litellm-http.workspace = true
|
|||
litellm-llms.workspace = true
|
||||
litellm-router.workspace = true
|
||||
litellm-secrets.workspace = true
|
||||
litellm-types.workspace = true
|
||||
litellm-llms-types.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use litellm_cache_response::{CacheOptions, CacheScope};
|
||||
use litellm_cache_response::{CacheOptions, CachePolicy, CacheScope};
|
||||
use litellm_gateway_auth::AuthenticatedRequest;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value};
|
||||
|
|
@ -38,11 +38,13 @@ pub(crate) fn prepare(
|
|||
.map_err(|error| Error::InvalidBody(error.to_string()))?;
|
||||
let caller = identity.caller();
|
||||
let options = CacheOptions {
|
||||
caching,
|
||||
no_cache: controls.no_cache,
|
||||
no_store: controls.no_store,
|
||||
ttl: controls.ttl.map(duration).transpose()?,
|
||||
max_age: controls.max_age.map(duration).transpose()?,
|
||||
policy: CachePolicy {
|
||||
caching,
|
||||
no_cache: controls.no_cache,
|
||||
no_store: controls.no_store,
|
||||
ttl: controls.ttl.map(duration).transpose()?,
|
||||
max_age: controls.max_age.map(duration).transpose()?,
|
||||
},
|
||||
scope: CacheScope::Isolated(
|
||||
serde_json::json!([
|
||||
caller.principal().authority(),
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ async fn handle(
|
|||
extra_headers: None,
|
||||
timeout: deployment.timeout,
|
||||
},
|
||||
cache_options,
|
||||
cache_options.policy,
|
||||
),
|
||||
(),
|
||||
headers.clone(),
|
||||
|
|
|
|||
|
|
@ -68,11 +68,7 @@ impl Gateway {
|
|||
auth.clone(),
|
||||
secrets.clone(),
|
||||
),
|
||||
messages: MessagesRoute::builder()
|
||||
.with_http(provider.clone())
|
||||
.with_auth(auth.clone())
|
||||
.with_secrets(secrets.clone())
|
||||
.build(),
|
||||
messages: MessagesRoute::new(provider.clone(), auth.clone(), secrets.clone()),
|
||||
responses: ResponsesRoute::new(provider, auth.clone(), secrets.clone()),
|
||||
ocr: OcrRoute::new(OcrClient::new(
|
||||
&resources.pool,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use axum::{
|
|||
};
|
||||
use litellm_core::messages::{MessagesCall, messages_body, route::Messages};
|
||||
use litellm_host_http::Sse;
|
||||
use litellm_types::utils::{ProviderSpecificHeader, ProviderSpecificHeaders};
|
||||
use litellm_llms_types::headers::{ProviderSpecificHeader, ProviderSpecificHeaders};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::{Deployment, Error, Gateway, JsonObject, RequestId, request};
|
||||
|
|
@ -54,7 +54,7 @@ async fn handle(
|
|||
};
|
||||
|
||||
let call = project(deployment, body, headers)?;
|
||||
let machine = route.machine(call, cache_options);
|
||||
let machine = route.machine(call, cache_options.policy);
|
||||
let stream =
|
||||
Sse::<Messages, _, _>::new(Json, |error| Bytes::from(Error::from(error).sse_frame()));
|
||||
let headers = crate::caching::CacheHeaders::default();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ use std::sync::Arc;
|
|||
use axum::{Json, extract::State, http::HeaderMap, response::IntoResponse};
|
||||
use litellm_auth::SecretValue;
|
||||
use litellm_core::ocr::types::{LiteLLMOcrRequest, OcrConnectionInputs, OcrDocumentInput};
|
||||
use litellm_llms::base_llm::ocr::transformation::OcrDocument;
|
||||
use litellm_llms::base_llm::ocr::transformation::decode_request_value;
|
||||
use litellm_llms_types::formats::ocr::OcrDocument;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
|
|
@ -42,7 +43,11 @@ async fn handle(
|
|||
file_name: upload.file_name,
|
||||
mime_type: upload.mime_type,
|
||||
},
|
||||
None => OcrDocument::try_from(body.get("document").cloned().unwrap_or_default())?.into(),
|
||||
None => decode_request_value::<OcrDocument>(
|
||||
body.get("document").cloned().unwrap_or_default(),
|
||||
"document",
|
||||
)?
|
||||
.into(),
|
||||
};
|
||||
let format = body
|
||||
.get("req_format")
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ pub(crate) async fn create(
|
|||
extra_headers: None,
|
||||
timeout: deployment.timeout,
|
||||
};
|
||||
let machine = route.machine(call, cache_options);
|
||||
let machine = route.machine(call, cache_options.policy);
|
||||
let stream = Sse::<Responses, _, _>::new(Json, |error| {
|
||||
let error = Error::from(error);
|
||||
Bytes::from(format!(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ mod support;
|
|||
|
||||
use axum::{body::Body, http::Request};
|
||||
use litellm_gateway_inference::Error;
|
||||
use litellm_llms::base_llm::ocr::{error::Error as OcrError, transformation::OcrDocument};
|
||||
use litellm_llms::base_llm::ocr::{error::Error as OcrError, transformation::decode_request_value};
|
||||
use litellm_llms_types::formats::ocr::OcrDocument;
|
||||
use rstest::rstest;
|
||||
use serde_json::{Value, json};
|
||||
use tower::ServiceExt;
|
||||
|
|
@ -146,7 +147,7 @@ async fn malformed_multipart_uses_an_openai_error_envelope(
|
|||
#[rstest]
|
||||
#[case::missing_document(
|
||||
"/v1/ocr", "mistral/test-ocr", "",
|
||||
Error::Ocr(OcrDocument::try_from(Value::Null).unwrap_err()),
|
||||
Error::Ocr(decode_request_value::<OcrDocument>(Value::Null, "document").unwrap_err()),
|
||||
)]
|
||||
#[case::empty_document(
|
||||
"/v1/ocr",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
The same ownership rule applies to Messages, Responses, Chat Completions, OCR, and other API formats. This crate owns their shared API data contracts. Adapter contracts and shared transformation machinery belong in `llms/src/base_llm/<format>/`, provider policy in `llms/src/<provider>/<format>/`, and call orchestration in `core/src/<format>/`. A provider originating a format, or several providers using a type, does not change these responsibilities. Existing model locations outside this crate are not exceptions to this rule for new shared API contracts
|
||||
|
||||
- `litellm-types` owns shared API data contracts and their serialization
|
||||
- `litellm-llms-types` owns shared API data contracts and their serialization
|
||||
- A type belongs here when it describes a request, response, event, or value that consumers must agree on independently of how a call executes
|
||||
- Being public, serializable, or used by several crates is not sufficient
|
||||
- These are intended boundaries, not a claim that every existing item follows them
|
||||
|
||||
- Organize public contracts by API format: `messages`, `chat_completions`, and `responses`
|
||||
- Use names such as `litellm_types::messages::MessagesRequest`, without an Anthropic prefix solely because Anthropic designed Messages
|
||||
- Existing `llms::openai`, `llms::anthropic_messages`, and chat types under `utils` are legacy locations, not patterns for new modules
|
||||
- Organize public API contracts under `formats`: `messages`, `chat_completions`, `responses`, `ocr`, `audio_transcription`, and `batches`
|
||||
- Use names such as `litellm_llms_types::formats::messages::MessagesRequest`, without an Anthropic prefix solely because Anthropic designed Messages
|
||||
- Keep one canonical definition and import path when moving a contract, updating consumers together instead of adding duplicate models or compatibility re-exports
|
||||
|
||||
- Keep shared provider-specific wire types and extensions under `providers`
|
||||
- Provider types may reuse format types; format types must not depend on provider types
|
||||
- A field belonging to an API format stays under `formats` even when provider support varies. Including it in a type does not promise provider support
|
||||
- Add a typed provider extension when a consumer needs to interpret or construct it. Keep adapter-only projections in `llms` until a shared public data contract is needed
|
||||
- Keep one authoritative representation of each field, preserving unknown fields without duplicating typed values in an extension map
|
||||
- Provider capability checks, defaults, authentication, header selection, and transformations remain in `llms`
|
||||
|
||||
- Keep format-independent data helpers such as `headers`, `recognized`, and `serde_compat` at the crate root
|
||||
|
||||
- Shared request/response bodies, message and content-block enums, usage records, tool-call chunks, stream-event payloads, and protocol error bodies belong here
|
||||
- This includes LiteLLM's normalized response contracts and extensions, not just exact upstream schemas
|
||||
- `ChatCompletionsResponse` currently represents the response handed to the host, so replacing it with a supposedly more complete upstream schema must not silently change that contract
|
||||
|
|
@ -31,6 +39,7 @@ The same ownership rule applies to Messages, Responses, Chat Completions, OCR, a
|
|||
- Provider config traits, `MessagesTransformContext`, `MessagesModelCapabilities`, `ThinkingBudgets`, `StreamShape`, and transformer state belong in `llms`
|
||||
- Catalog records and pricing belong in `model-catalog`, which may reuse wire enums such as `ReasoningEffort`
|
||||
- Host hooks, Python objects, credentials, clients, timeouts, and routing decisions do not become API payload types merely because they cross a crate boundary
|
||||
- Legacy logging operation selection belongs in `callbacks-legacy-python`, not this crate
|
||||
|
||||
- Stream-event data belongs here, but live streams, decoders, framing, buffering, and stream lifecycle decisions do not
|
||||
- Keep SSE and AWS framing in `framer`, provider decoding and conversion in `llms`, and call orchestration in `core`
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "litellm-types"
|
||||
name = "litellm-llms-types"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
|
@ -9,9 +9,11 @@ repository.workspace = true
|
|||
schema = ["dep:schemars"]
|
||||
|
||||
[dependencies]
|
||||
macro_rules_attribute.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_with.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct AudioTranscriptionResponseData {
|
||||
pub text: String,
|
||||
}
|
||||
36
litellm-rust/crates/llms-types/src/formats/batches.rs
Normal file
36
litellm-rust/crates/llms-types/src/formats/batches.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Copy, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BatchStatus {
|
||||
InProgress,
|
||||
Cancelling,
|
||||
Completed,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Eq)]
|
||||
pub struct BatchRequestCounts {
|
||||
pub total: u64,
|
||||
pub completed: u64,
|
||||
pub failed: u64,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Eq)]
|
||||
pub struct BatchResponse {
|
||||
pub id: String,
|
||||
pub object: String,
|
||||
pub endpoint: String,
|
||||
pub input_file_id: String,
|
||||
pub completion_window: String,
|
||||
pub status: BatchStatus,
|
||||
pub output_file_id: String,
|
||||
pub created_at: i64,
|
||||
pub in_progress_at: Option<i64>,
|
||||
pub expires_at: Option<i64>,
|
||||
pub completed_at: Option<i64>,
|
||||
pub expired_at: Option<i64>,
|
||||
pub cancelling_at: Option<i64>,
|
||||
pub cancelled_at: Option<i64>,
|
||||
pub request_counts: BatchRequestCounts,
|
||||
}
|
||||
223
litellm-rust/crates/llms-types/src/formats/chat_completions.rs
Normal file
223
litellm-rust/crates/llms-types/src/formats/chat_completions.rs
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
use serde_json::{Map, Value};
|
||||
use strum::IntoStaticStr;
|
||||
|
||||
/// Reasoning effort level accepted or applied by the model.
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Copy, Eq, IntoStaticStr)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ReasoningEffort {
|
||||
None,
|
||||
Minimal,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Xhigh,
|
||||
Max,
|
||||
}
|
||||
|
||||
impl ReasoningEffort {
|
||||
pub const ALL: [Self; 7] = [
|
||||
Self::None,
|
||||
Self::Minimal,
|
||||
Self::Low,
|
||||
Self::Medium,
|
||||
Self::High,
|
||||
Self::Xhigh,
|
||||
Self::Max,
|
||||
];
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
self.into()
|
||||
}
|
||||
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
Self::ALL
|
||||
.into_iter()
|
||||
.find(|effort| effort.as_str() == value)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[serde(untagged)]
|
||||
pub enum ChatMessageContent {
|
||||
Text(String),
|
||||
Parts(Vec<Value>),
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct ChatMessage {
|
||||
pub role: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<ChatMessageContent>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct ChatCompletionToolCallFunctionChunk {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
pub arguments: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider_specific_fields: Option<Map<String, Value>>,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct ChatCompletionToolCallChunk {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
pub tool_type: String,
|
||||
pub function: ChatCompletionToolCallFunctionChunk,
|
||||
pub index: i64,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ChatCompletionThinkingBlock {
|
||||
Thinking {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
thinking: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
signature: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
cache_control: Option<Value>,
|
||||
},
|
||||
RedactedThinking {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
data: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
cache_control: Option<Value>,
|
||||
},
|
||||
}
|
||||
|
||||
/// OpenAI `usage`, including the `prompt_tokens_details` split LiteLLM's Python
|
||||
/// path reports so cost tracking sees the same numbers on either path.
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct PromptTokensDetails {
|
||||
pub cached_tokens: u64,
|
||||
pub cache_creation_tokens: u64,
|
||||
pub text_tokens: u64,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct ChatCompletionsUsage {
|
||||
pub prompt_tokens: u64,
|
||||
pub completion_tokens: u64,
|
||||
pub total_tokens: u64,
|
||||
pub prompt_tokens_details: PromptTokensDetails,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct ChatCompletionsChoiceMessage {
|
||||
pub role: String,
|
||||
// Whether an empty turn is `None` or `""` is the provider's choice, not a
|
||||
// shared invariant: Anthropic's transform ends on `merged_text or None`
|
||||
// while Converse assigns the joined string unconditionally. Each config
|
||||
// mirrors its own, so keep this optional and serialize it even when None.
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct ChatCompletionsChoice {
|
||||
pub index: u64,
|
||||
pub message: ChatCompletionsChoiceMessage,
|
||||
pub finish_reason: String,
|
||||
}
|
||||
|
||||
/// The normalized response handed back to the host.
|
||||
///
|
||||
/// There is deliberately no `id`: Python mints the `chatcmpl-…` id on the
|
||||
/// `ModelResponse` it already created, and echoing the provider's own id here
|
||||
/// would change it. Pinned by `response_carries_no_id` in the Anthropic chat transformation tests.
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct ChatCompletionsResponse {
|
||||
pub created: u64,
|
||||
pub model: String,
|
||||
pub choices: Vec<ChatCompletionsChoice>,
|
||||
pub usage: ChatCompletionsUsage,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct ChatCompletionDelta {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub role: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<ChatCompletionToolCallChunk>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning_content: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub thinking_blocks: Option<Vec<ChatCompletionThinkingBlock>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider_specific_fields: Option<Map<String, Value>>,
|
||||
#[serde(flatten)]
|
||||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct ChatCompletionStreamingChoice {
|
||||
pub index: u64,
|
||||
pub delta: ChatCompletionDelta,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub finish_reason: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub logprobs: Option<Value>,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct ChatCompletionChunk {
|
||||
pub id: String,
|
||||
pub created: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
pub object: String,
|
||||
pub choices: Vec<ChatCompletionStreamingChoice>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<ChatCompletionsUsage>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider_specific_fields: Option<Map<String, Value>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn reasoning_effort_names_match_the_wire_and_parse_back(
|
||||
#[values(
|
||||
ReasoningEffort::None,
|
||||
ReasoningEffort::Minimal,
|
||||
ReasoningEffort::Low,
|
||||
ReasoningEffort::Medium,
|
||||
ReasoningEffort::High,
|
||||
ReasoningEffort::Xhigh,
|
||||
ReasoningEffort::Max
|
||||
)]
|
||||
effort: ReasoningEffort,
|
||||
) {
|
||||
assert_eq!(
|
||||
serde_json::to_value(effort).unwrap(),
|
||||
Value::String(effort.as_str().to_string())
|
||||
);
|
||||
assert_eq!(ReasoningEffort::parse(effort.as_str()), Some(effort));
|
||||
assert!(ReasoningEffort::ALL.contains(&effort));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::unknown("ultra")]
|
||||
#[case::uppercase("HIGH")]
|
||||
#[case::empty("")]
|
||||
fn reasoning_effort_parse_rejects(#[case] value: &str) {
|
||||
assert_eq!(ReasoningEffort::parse(value), None);
|
||||
}
|
||||
}
|
||||
11
litellm-rust/crates/llms-types/src/formats/messages/mod.rs
Normal file
11
litellm-rust/crates/llms-types/src/formats/messages/mod.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
mod request;
|
||||
mod response;
|
||||
pub mod streaming;
|
||||
|
||||
pub use request::{
|
||||
AdaptiveThinking, CacheControl, ContentBlock, ContentBlockType, ContextEdit, ContextManagement,
|
||||
DisabledThinking, EffortLevel, EnabledThinking, Message, MessageContent,
|
||||
MessagesOptionalParams, MessagesRequest, MessagesTool, OutputConfig, Speed, SystemPrompt,
|
||||
ThinkingConfig, ThinkingDisplay,
|
||||
};
|
||||
pub use response::MessagesResponse;
|
||||
|
|
@ -1,26 +1,25 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use strum::IntoStaticStr;
|
||||
|
||||
use crate::{llms::openai::ReasoningEffort, recognized::Recognized};
|
||||
use crate::formats::chat_completions::ReasoningEffort;
|
||||
use crate::recognized::Recognized;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[serde(untagged)]
|
||||
pub enum SystemPrompt {
|
||||
Text(String),
|
||||
Blocks(Vec<ContentBlock>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[serde(untagged)]
|
||||
pub enum MessageContent {
|
||||
Text(String),
|
||||
Blocks(Vec<ContentBlock>),
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display, strum::EnumString,
|
||||
)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Eq, strum::Display, strum::EnumString)]
|
||||
#[serde(from = "String", into = "String")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ContentBlockType {
|
||||
|
|
@ -49,7 +48,8 @@ impl From<ContentBlockType> for String {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct ContentBlock {
|
||||
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
|
||||
pub block_type: Option<ContentBlockType>,
|
||||
|
|
@ -93,7 +93,8 @@ impl ContentBlock {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct CacheControl {
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
pub cache_type: Option<String>,
|
||||
|
|
@ -105,15 +106,16 @@ pub struct CacheControl {
|
|||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AnthropicMessage {
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct Message {
|
||||
pub role: String,
|
||||
pub content: MessageContent,
|
||||
#[serde(flatten)]
|
||||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, IntoStaticStr, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Copy, Hash, IntoStaticStr, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum EffortLevel {
|
||||
|
|
@ -142,7 +144,8 @@ impl From<EffortLevel> for ReasoningEffort {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, IntoStaticStr, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Copy, IntoStaticStr, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum Speed {
|
||||
|
|
@ -158,9 +161,9 @@ impl Speed {
|
|||
|
||||
/// The tools whose presence changes how the request is sent. Every other tool, custom or
|
||||
/// server, deserializes as `Recognized::Unrecognized` and passes through verbatim.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum AnthropicTool {
|
||||
pub enum MessagesTool {
|
||||
#[serde(rename = "advisor_20260301")]
|
||||
Advisor {
|
||||
#[serde(flatten)]
|
||||
|
|
@ -178,7 +181,7 @@ pub enum AnthropicTool {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ContextEdit {
|
||||
#[serde(rename = "compact_20260112")]
|
||||
|
|
@ -198,7 +201,8 @@ pub enum ContextEdit {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct ContextManagement {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub edits: Option<Vec<Recognized<ContextEdit>>>,
|
||||
|
|
@ -206,7 +210,8 @@ pub struct ContextManagement {
|
|||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct OutputConfig {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub effort: Option<Recognized<EffortLevel>>,
|
||||
|
|
@ -222,7 +227,8 @@ impl OutputConfig {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Copy, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ThinkingDisplay {
|
||||
Summarized,
|
||||
|
|
@ -230,7 +236,8 @@ pub enum ThinkingDisplay {
|
|||
Updates,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct EnabledThinking {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub budget_tokens: Option<Recognized<u64>>,
|
||||
|
|
@ -240,7 +247,8 @@ pub struct EnabledThinking {
|
|||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct AdaptiveThinking {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub display: Option<Recognized<ThinkingDisplay>>,
|
||||
|
|
@ -248,13 +256,14 @@ pub struct AdaptiveThinking {
|
|||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct DisabledThinking {
|
||||
#[serde(flatten)]
|
||||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
pub enum ThinkingConfig {
|
||||
Enabled(EnabledThinking),
|
||||
|
|
@ -278,16 +287,17 @@ impl ThinkingConfig {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AnthropicMessagesRequest {
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct MessagesRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<AnthropicMessage>,
|
||||
pub messages: Vec<Message>,
|
||||
#[serde(flatten)]
|
||||
pub params: AnthropicMessagesOptionalParams,
|
||||
pub params: MessagesOptionalParams,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AnthropicMessagesOptionalParams {
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct MessagesOptionalParams {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_tokens: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -305,7 +315,7 @@ pub struct AnthropicMessagesOptionalParams {
|
|||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub top_k: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<Recognized<AnthropicTool>>>,
|
||||
pub tools: Option<Vec<Recognized<MessagesTool>>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_choice: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -334,7 +344,7 @@ pub struct AnthropicMessagesOptionalParams {
|
|||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
impl AnthropicMessage {
|
||||
impl Message {
|
||||
pub fn blocks(&self) -> &[ContentBlock] {
|
||||
match &self.content {
|
||||
MessageContent::Blocks(blocks) => blocks,
|
||||
|
|
@ -357,7 +367,7 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
|
||||
fn round_trip<T: serde::de::DeserializeOwned + Serialize>(value: &Value) -> Value {
|
||||
fn round_trip<T: serde::de::DeserializeOwned + serde::Serialize>(value: &Value) -> Value {
|
||||
let parsed: T = serde_json::from_value(value.clone()).unwrap();
|
||||
serde_json::to_value(parsed).unwrap()
|
||||
}
|
||||
|
|
@ -396,7 +406,7 @@ mod tests {
|
|||
"stream": true,
|
||||
"safeguards": [{"type": "dangerous_tool_use"}]
|
||||
});
|
||||
let request: AnthropicMessagesRequest = serde_json::from_value(body.clone()).unwrap();
|
||||
let request: MessagesRequest = serde_json::from_value(body.clone()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
|
|
@ -432,7 +442,7 @@ mod tests {
|
|||
#[case] message: Value,
|
||||
#[case] expected: Vec<ContentBlock>,
|
||||
) {
|
||||
let message: AnthropicMessage = serde_json::from_value(message).unwrap();
|
||||
let message: Message = serde_json::from_value(message).unwrap();
|
||||
assert_eq!(message.blocks(), expected.as_slice());
|
||||
}
|
||||
|
||||
|
|
@ -440,7 +450,7 @@ mod tests {
|
|||
#[case::replaces_string_content(json!({"role": "assistant", "content": "old", "name": "kept"}))]
|
||||
#[case::replaces_block_content(json!({"role": "assistant", "content": [{"type": "text", "text": "old"}], "name": "kept"}))]
|
||||
fn with_blocks_replaces_content_and_keeps_the_rest(#[case] message: Value) {
|
||||
let message: AnthropicMessage = serde_json::from_value(message).unwrap();
|
||||
let message: Message = serde_json::from_value(message).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(message.with_blocks(vec![ContentBlock::text("new")])).unwrap(),
|
||||
json!({"role": "assistant", "content": [{"type": "text", "text": "new"}], "name": "kept"})
|
||||
|
|
@ -506,7 +516,7 @@ mod tests {
|
|||
"context_management": [{"type": "compaction", "compact_threshold": 5}]
|
||||
}))]
|
||||
fn request_round_trips_unchanged(#[case] request: Value) {
|
||||
assert_eq!(round_trip::<AnthropicMessagesRequest>(&request), request);
|
||||
assert_eq!(round_trip::<MessagesRequest>(&request), request);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
|
|
@ -555,15 +565,15 @@ mod tests {
|
|||
#[rstest]
|
||||
#[case::advisor(
|
||||
json!({"type": "advisor_20260301", "name": "advisor"}),
|
||||
Recognized::Known(AnthropicTool::Advisor { extra: Map::from_iter([("name".to_string(), json!("advisor"))]) })
|
||||
Recognized::Known(MessagesTool::Advisor { extra: Map::from_iter([("name".to_string(), json!("advisor"))]) })
|
||||
)]
|
||||
#[case::regex_tool_search(
|
||||
json!({"type": "tool_search_tool_regex_20251119"}),
|
||||
Recognized::Known(AnthropicTool::ToolSearchRegex { extra: Map::new() })
|
||||
Recognized::Known(MessagesTool::ToolSearchRegex { extra: Map::new() })
|
||||
)]
|
||||
#[case::bm25_tool_search(
|
||||
json!({"type": "tool_search_tool_bm25_20251119"}),
|
||||
Recognized::Known(AnthropicTool::ToolSearchBm25 { extra: Map::new() })
|
||||
Recognized::Known(MessagesTool::ToolSearchBm25 { extra: Map::new() })
|
||||
)]
|
||||
#[case::custom_tool_without_a_type(
|
||||
json!({"name": "advisor", "input_schema": {}}),
|
||||
|
|
@ -576,10 +586,10 @@ mod tests {
|
|||
#[case::not_an_object(json!("advisor_20260301"), Recognized::Unrecognized(json!("advisor_20260301")))]
|
||||
fn tools_are_recognized_by_their_exact_type(
|
||||
#[case] tool: Value,
|
||||
#[case] expected: Recognized<AnthropicTool>,
|
||||
#[case] expected: Recognized<MessagesTool>,
|
||||
) {
|
||||
assert_eq!(
|
||||
serde_json::from_value::<Recognized<AnthropicTool>>(tool).unwrap(),
|
||||
serde_json::from_value::<Recognized<MessagesTool>>(tool).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AnthropicMessagesResponse {
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct MessagesResponse {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub message_type: String,
|
||||
|
|
@ -31,8 +30,8 @@ mod tests {
|
|||
stop_sequence: Option<&str>,
|
||||
usage: Option<Value>,
|
||||
container: Option<Value>,
|
||||
) -> AnthropicMessagesResponse {
|
||||
AnthropicMessagesResponse {
|
||||
) -> MessagesResponse {
|
||||
MessagesResponse {
|
||||
id: "msg_1".to_string(),
|
||||
message_type: "message".to_string(),
|
||||
role: "assistant".to_string(),
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct MessagesStreamUsage {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub input_tokens: Option<u64>,
|
||||
|
|
@ -17,7 +17,7 @@ pub struct MessagesStreamUsage {
|
|||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct MessagesStreamMessage {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
|
|
@ -32,7 +32,7 @@ pub struct MessagesStreamMessage {
|
|||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum MessagesContentBlockDelta {
|
||||
TextDelta {
|
||||
|
|
@ -56,7 +56,7 @@ pub enum MessagesContentBlockDelta {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct MessagesContentBlock {
|
||||
#[serde(rename = "type")]
|
||||
pub block_type: String,
|
||||
|
|
@ -82,7 +82,8 @@ pub struct MessagesContentBlock {
|
|||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct MessagesDelta {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stop_reason: Option<String>,
|
||||
|
|
@ -96,7 +97,7 @@ pub struct MessagesDelta {
|
|||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct MessagesStreamError {
|
||||
#[serde(rename = "type")]
|
||||
pub error_type: String,
|
||||
|
|
@ -107,7 +108,7 @@ pub struct MessagesStreamError {
|
|||
pub extra: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum MessagesStreamEvent {
|
||||
MessageStart {
|
||||
6
litellm-rust/crates/llms-types/src/formats/mod.rs
Normal file
6
litellm-rust/crates/llms-types/src/formats/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod audio_transcription;
|
||||
pub mod batches;
|
||||
pub mod chat_completions;
|
||||
pub mod messages;
|
||||
pub mod ocr;
|
||||
pub mod responses;
|
||||
152
litellm-rust/crates/llms-types/src/formats/ocr.rs
Normal file
152
litellm-rust/crates/llms-types/src/formats/ocr.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
use serde_with::serde_as;
|
||||
|
||||
use crate::serde_compat::{FiniteF64, LaxI64};
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum OcrDocument {
|
||||
#[serde(rename = "document_url")]
|
||||
DocumentUrl {
|
||||
document_url: String,
|
||||
#[serde(flatten)]
|
||||
extra_fields: BTreeMap<String, Option<String>>,
|
||||
},
|
||||
#[serde(rename = "image_url")]
|
||||
ImageUrl {
|
||||
image_url: String,
|
||||
#[serde(flatten)]
|
||||
extra_fields: BTreeMap<String, Option<String>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl OcrDocument {
|
||||
pub fn source(&self) -> &str {
|
||||
match self {
|
||||
Self::DocumentUrl { document_url, .. } => document_url,
|
||||
Self::ImageUrl { image_url, .. } => image_url,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_remote(&self) -> bool {
|
||||
let source = self.source();
|
||||
source.starts_with("http://") || source.starts_with("https://")
|
||||
}
|
||||
|
||||
pub fn with_source(self, source: String) -> Self {
|
||||
match self {
|
||||
Self::DocumentUrl { extra_fields, .. } => Self::DocumentUrl {
|
||||
document_url: source,
|
||||
extra_fields,
|
||||
},
|
||||
Self::ImageUrl { extra_fields, .. } => Self::ImageUrl {
|
||||
image_url: source,
|
||||
extra_fields,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Copy, Default, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OcrResponseFormat {
|
||||
#[default]
|
||||
Litellm,
|
||||
Native,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct OcrPageDimensions {
|
||||
#[serde_as(deserialize_as = "Option<LaxI64>")]
|
||||
pub dpi: Option<i64>,
|
||||
#[serde_as(deserialize_as = "Option<LaxI64>")]
|
||||
pub height: Option<i64>,
|
||||
#[serde_as(deserialize_as = "Option<LaxI64>")]
|
||||
pub width: Option<i64>,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct OcrPageImage {
|
||||
pub image_base64: Option<String>,
|
||||
pub bbox: Option<Map<String, Value>>,
|
||||
#[serde(flatten)]
|
||||
pub extra_fields: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct OcrPage {
|
||||
#[serde_as(deserialize_as = "LaxI64")]
|
||||
pub index: i64,
|
||||
pub markdown: String,
|
||||
pub images: Option<Vec<OcrPageImage>>,
|
||||
pub dimensions: Option<OcrPageDimensions>,
|
||||
#[serde(flatten)]
|
||||
pub extra_fields: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct OcrUsageInfo {
|
||||
#[serde_as(deserialize_as = "Option<LaxI64>")]
|
||||
pub pages_processed: Option<i64>,
|
||||
#[serde_as(deserialize_as = "Option<LaxI64>")]
|
||||
pub pages_processed_annotation: Option<i64>,
|
||||
#[serde_as(deserialize_as = "Option<FiniteF64>")]
|
||||
pub credits: Option<f64>,
|
||||
#[serde_as(deserialize_as = "Option<LaxI64>")]
|
||||
pub doc_size_bytes: Option<i64>,
|
||||
#[serde(flatten)]
|
||||
pub extra_fields: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct LiteLLMOcrResponse {
|
||||
pub pages: Vec<OcrPage>,
|
||||
pub model: String,
|
||||
pub document_annotation: Option<Value>,
|
||||
pub usage_info: Option<OcrUsageInfo>,
|
||||
pub content: Option<String>,
|
||||
pub tables: Option<Vec<Map<String, Value>>>,
|
||||
#[serde(rename = "keyValuePairs")]
|
||||
pub key_value_pairs: Option<Vec<Map<String, Value>>>,
|
||||
#[serde(default = "ocr_object")]
|
||||
pub object: String,
|
||||
#[serde(flatten)]
|
||||
pub extra_fields: Map<String, Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub provider_native_response: Option<Map<String, Value>>,
|
||||
}
|
||||
|
||||
impl LiteLLMOcrResponse {
|
||||
pub fn new(model: impl Into<String>, pages: Vec<OcrPage>) -> Self {
|
||||
Self {
|
||||
pages,
|
||||
model: model.into(),
|
||||
document_annotation: None,
|
||||
usage_info: None,
|
||||
content: None,
|
||||
tables: None,
|
||||
key_value_pairs: None,
|
||||
object: ocr_object(),
|
||||
extra_fields: Map::new(),
|
||||
provider_native_response: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_json(self) -> Value {
|
||||
serde_json::to_value(self).expect("OCR response fields are JSON-compatible")
|
||||
}
|
||||
}
|
||||
|
||||
fn ocr_object() -> String {
|
||||
"ocr".into()
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
mod response;
|
||||
pub mod streaming_websocket;
|
||||
|
||||
pub use response::ResponsesApiResponse;
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct ResponsesApiResponse {
|
||||
pub id: String,
|
||||
pub model: String,
|
||||
|
|
@ -2,6 +2,8 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|||
use serde_json::{Map, Value};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, strum::AsRefStr)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(feature = "schema", schemars(with = "String"))]
|
||||
pub enum ResponsesWsEventType {
|
||||
#[strum(serialize = "response.create")]
|
||||
ResponseCreate,
|
||||
|
|
@ -52,7 +54,7 @@ impl<'de> Deserialize<'de> for ResponsesWsEventType {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct ResponsesWsEvent {
|
||||
#[serde(rename = "type")]
|
||||
pub event_type: ResponsesWsEventType,
|
||||
|
|
@ -78,7 +80,8 @@ impl ResponsesWsEvent {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Eq)]
|
||||
pub struct ResponsesErrorFrame {
|
||||
#[serde(rename = "type")]
|
||||
pub frame_type: &'static str,
|
||||
|
|
@ -97,7 +100,8 @@ impl ResponsesErrorFrame {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Eq)]
|
||||
pub struct ResponsesErrorBody {
|
||||
#[serde(rename = "type")]
|
||||
pub error_type: &'static str,
|
||||
17
litellm-rust/crates/llms-types/src/headers.rs
Normal file
17
litellm-rust/crates/llms-types/src/headers.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use serde_json::{Map, Value};
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Default)]
|
||||
pub struct ProviderSpecificHeader {
|
||||
#[serde(default)]
|
||||
pub custom_llm_provider: String,
|
||||
#[serde(default)]
|
||||
pub extra_headers: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[serde(untagged)]
|
||||
pub enum ProviderSpecificHeaders {
|
||||
One(ProviderSpecificHeader),
|
||||
Many(Vec<ProviderSpecificHeader>),
|
||||
}
|
||||
11
litellm-rust/crates/llms-types/src/lib.rs
Normal file
11
litellm-rust/crates/llms-types/src/lib.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
macro_rules_attribute::attribute_alias! {
|
||||
#[apply(wire_type)] =
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))];
|
||||
}
|
||||
|
||||
pub mod formats;
|
||||
pub mod headers;
|
||||
pub mod providers;
|
||||
pub mod recognized;
|
||||
pub mod serde_compat;
|
||||
1
litellm-rust/crates/llms-types/src/providers/mod.rs
Normal file
1
litellm-rust/crates/llms-types/src/providers/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod anthropic;
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[serde(untagged)]
|
||||
pub enum Recognized<T> {
|
||||
Known(T),
|
||||
113
litellm-rust/crates/llms-types/src/serde_compat.rs
Normal file
113
litellm-rust/crates/llms-types/src/serde_compat.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use serde::{
|
||||
Deserializer,
|
||||
de::{Error, Visitor},
|
||||
};
|
||||
use serde_with::DeserializeAs;
|
||||
|
||||
pub struct LaxI64;
|
||||
pub struct FiniteF64;
|
||||
|
||||
impl<'de> DeserializeAs<'de, i64> for LaxI64 {
|
||||
fn deserialize_as<D: Deserializer<'de>>(deserializer: D) -> Result<i64, D::Error> {
|
||||
deserializer.deserialize_any(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Visitor<'de> for LaxI64 {
|
||||
type Value = i64;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("an integer in the i64 range")
|
||||
}
|
||||
|
||||
fn visit_i64<E: Error>(self, value: i64) -> Result<i64, E> {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn visit_u64<E: Error>(self, value: u64) -> Result<i64, E> {
|
||||
i64::try_from(value).map_err(E::custom)
|
||||
}
|
||||
|
||||
fn visit_f64<E: Error>(self, value: f64) -> Result<i64, E> {
|
||||
integral_float(value).ok_or_else(|| E::custom("expected an integer in the i64 range"))
|
||||
}
|
||||
|
||||
fn visit_str<E: Error>(self, value: &str) -> Result<i64, E> {
|
||||
integer_string(value.trim())
|
||||
.ok_or_else(|| E::custom("expected an integer in the i64 range"))
|
||||
}
|
||||
|
||||
fn visit_bool<E: Error>(self, value: bool) -> Result<i64, E> {
|
||||
Ok(i64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeAs<'de, f64> for FiniteF64 {
|
||||
fn deserialize_as<D: Deserializer<'de>>(deserializer: D) -> Result<f64, D::Error> {
|
||||
deserializer.deserialize_any(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Visitor<'de> for FiniteF64 {
|
||||
type Value = f64;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("a finite number")
|
||||
}
|
||||
|
||||
fn visit_i64<E: Error>(self, value: i64) -> Result<f64, E> {
|
||||
Ok(value as f64)
|
||||
}
|
||||
|
||||
fn visit_u64<E: Error>(self, value: u64) -> Result<f64, E> {
|
||||
Ok(value as f64)
|
||||
}
|
||||
|
||||
fn visit_f64<E: Error>(self, value: f64) -> Result<f64, E> {
|
||||
value
|
||||
.is_finite()
|
||||
.then_some(value)
|
||||
.ok_or_else(|| E::custom("expected a finite number"))
|
||||
}
|
||||
|
||||
fn visit_str<E: Error>(self, value: &str) -> Result<f64, E> {
|
||||
self.visit_f64(value.trim().parse::<f64>().map_err(E::custom)?)
|
||||
}
|
||||
|
||||
fn visit_bool<E: Error>(self, value: bool) -> Result<f64, E> {
|
||||
Ok(f64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
fn integer_string(value: &str) -> Option<i64> {
|
||||
let integer = match value.split_once('.') {
|
||||
Some((integer, fraction)) => {
|
||||
if fraction.is_empty() || !fraction.bytes().all(|byte| byte == b'0') {
|
||||
return None;
|
||||
}
|
||||
integer
|
||||
}
|
||||
None => value,
|
||||
};
|
||||
if integer.starts_with('_') || integer.ends_with('_') || integer.contains("__") {
|
||||
return None;
|
||||
}
|
||||
let digits = integer.strip_prefix(['+', '-']).unwrap_or(integer);
|
||||
if digits.is_empty()
|
||||
|| digits.starts_with('_')
|
||||
|| !digits
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_digit() || byte == b'_')
|
||||
{
|
||||
return None;
|
||||
}
|
||||
integer.replace('_', "").parse().ok()
|
||||
}
|
||||
|
||||
fn integral_float(value: f64) -> Option<i64> {
|
||||
(value.is_finite()
|
||||
&& value.fract() == 0.0
|
||||
&& value >= i64::MIN as f64
|
||||
&& value < -(i64::MIN as f64))
|
||||
.then_some(value as i64)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use litellm_types::llms::anthropic_messages::anthropic_request::{ContentBlock, ContentBlockType};
|
||||
use litellm_llms_types::formats::messages::{ContentBlock, ContentBlockType};
|
||||
use rstest::rstest;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use litellm_types::messages::streaming::MessagesStreamEvent;
|
||||
use litellm_llms_types::formats::messages::streaming::MessagesStreamEvent;
|
||||
use rstest::rstest;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
104
litellm-rust/crates/llms-types/tests/ocr.rs
Normal file
104
litellm-rust/crates/llms-types/tests/ocr.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use litellm_llms_types::formats::ocr::{LiteLLMOcrResponse, OcrDocument, OcrPage};
|
||||
use rstest::rstest;
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
#[rstest]
|
||||
#[case::missing_page_fields(json!({"pages": [{}]}))]
|
||||
#[case::invalid_markdown(json!({"pages": [{"index": 0, "markdown": false}]}))]
|
||||
#[case::invalid_image_bounds(json!({"pages": [{"index": 0, "markdown": "", "images": [{"bbox": []}]}]}))]
|
||||
#[case::fractional_page_count(json!({"usage_info": {"pages_processed": 1.5}}))]
|
||||
#[case::invalid_table(json!({"tables": [false]}))]
|
||||
#[case::invalid_key_value_pair(json!({"keyValuePairs": [[]]}))]
|
||||
#[case::invalid_native_response(json!({"provider_native_response": []}))]
|
||||
fn normalized_response_rejects_invalid_shared_fields(#[case] fields: Value) {
|
||||
let payload: Map<String, Value> = json!({"model": "model", "pages": []})
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.chain(fields.as_object().unwrap())
|
||||
.map(|(key, value)| (key.clone(), value.clone()))
|
||||
.collect();
|
||||
assert!(serde_json::from_value::<LiteLLMOcrResponse>(Value::Object(payload)).is_err());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn document_rejects_non_string_provider_fields() {
|
||||
assert!(
|
||||
serde_json::from_value::<OcrDocument>(json!({
|
||||
"type": "image_url", "image_url": "https://example.com/image", "detail": 42
|
||||
}))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::large_integer(json!("9007199254740993.0"), 9_007_199_254_740_993)]
|
||||
#[case::signed_decimal(json!("+2.000"), 2)]
|
||||
#[case::separator(json!("1_000"), 1000)]
|
||||
#[case::boolean(json!(true), 1)]
|
||||
#[case::integral_float(json!(2.0), 2)]
|
||||
fn numeric_coercion_preserves_integer_precision(#[case] value: Value, #[case] expected: i64) {
|
||||
let page: OcrPage = serde_json::from_value(json!({"index": value, "markdown": ""})).unwrap();
|
||||
assert_eq!(page.index, expected);
|
||||
assert_eq!(
|
||||
serde_json::to_value(page).unwrap()["index"],
|
||||
json!(expected)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::exponent(json!("1e2"))]
|
||||
#[case::missing_integer(json!(".0"))]
|
||||
#[case::missing_fraction(json!("2."))]
|
||||
#[case::leading_separator(json!("_2"))]
|
||||
#[case::repeated_separator(json!("2__0"))]
|
||||
#[case::fractional_float(json!(2.5))]
|
||||
#[case::null(json!(null))]
|
||||
fn page_index_rejects_invalid_integers(#[case] value: Value) {
|
||||
assert!(serde_json::from_value::<OcrPage>(json!({"index": value, "markdown": ""})).is_err());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::document_url("document_url", "document_name", "application/pdf")]
|
||||
#[case::image_url("image_url", "detail", "image/png")]
|
||||
fn document_variants_preserve_provider_fields_when_rewriting_sources(
|
||||
#[case] kind: &str,
|
||||
#[case] field: &str,
|
||||
#[case] mime_type: &str,
|
||||
#[values(json!("kept"), Value::Null)] extra: Value,
|
||||
) {
|
||||
let original = "https://example.com/input";
|
||||
let replacement = format!("data:{mime_type};base64,AA==");
|
||||
let document: OcrDocument =
|
||||
serde_json::from_value(json!({"type": kind, kind: original, field: extra})).unwrap();
|
||||
assert_eq!(document.source(), original);
|
||||
assert!(document.is_remote());
|
||||
let rewritten = document.with_source(replacement.clone());
|
||||
assert!(!rewritten.is_remote());
|
||||
assert_eq!(
|
||||
serde_json::to_value(rewritten).unwrap(),
|
||||
json!({"type": kind, kind: replacement, field: extra})
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::absent_native(None)]
|
||||
#[case::present_native(Some(Map::from_iter([("native".into(), json!({"nested": [null, 1]}))])))]
|
||||
fn response_serialization_preserves_extensions_and_native_presence(
|
||||
#[case] native: Option<Map<String, Value>>,
|
||||
) {
|
||||
let response = LiteLLMOcrResponse {
|
||||
extra_fields: Map::from_iter([("provider_field".into(), json!("kept"))]),
|
||||
provider_native_response: native.clone(),
|
||||
..LiteLLMOcrResponse::new("model", vec![])
|
||||
};
|
||||
let serialized = response.into_json();
|
||||
assert_eq!(serialized["provider_field"], "kept");
|
||||
assert_eq!(
|
||||
serialized.get("provider_native_response").cloned(),
|
||||
native.clone().map(Value::Object)
|
||||
);
|
||||
let decoded: LiteLLMOcrResponse = serde_json::from_value(serialized.clone()).unwrap();
|
||||
assert_eq!(decoded.provider_native_response, native);
|
||||
assert_eq!(decoded.into_json(), serialized);
|
||||
}
|
||||
86
litellm-rust/crates/llms-types/tests/serde_compat.rs
Normal file
86
litellm-rust/crates/llms-types/tests/serde_compat.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use litellm_llms_types::serde_compat::{FiniteF64, LaxI64};
|
||||
use rstest::rstest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use serde_with::serde_as;
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
struct Numbers {
|
||||
#[serde_as(deserialize_as = "Option<Vec<LaxI64>>")]
|
||||
integers: Option<Vec<i64>>,
|
||||
#[serde_as(deserialize_as = "Option<FiniteF64>")]
|
||||
float: Option<f64>,
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn adapters_compose_and_serialize_as_numbers() {
|
||||
let numbers: Numbers = serde_json::from_value(json!({
|
||||
"integers": ["9007199254740993.0", "1_000", " +2.000 ", 3.0, true],
|
||||
"float": " 1.5 "
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(numbers).unwrap(),
|
||||
json!({"integers": [9_007_199_254_740_993_i64, 1000, 2, 3, 1], "float": 1.5})
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::missing(json!({}))]
|
||||
#[case::null(json!({"integers": null, "float": null}))]
|
||||
fn optional_adapters_accept_missing_and_null_fields(#[case] input: Value) {
|
||||
assert_eq!(
|
||||
serde_json::from_value::<Numbers>(input).unwrap(),
|
||||
Numbers {
|
||||
integers: None,
|
||||
float: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::minimum(json!(i64::MIN), i64::MIN)]
|
||||
#[case::maximum(json!(i64::MAX), i64::MAX)]
|
||||
#[case::maximum_string(json!(i64::MAX.to_string()), i64::MAX)]
|
||||
fn integers_preserve_bounds(#[case] input: Value, #[case] expected: i64) {
|
||||
let numbers: Numbers = serde_json::from_value(json!({"integers": [input]})).unwrap();
|
||||
assert_eq!(numbers.integers, Some(vec![expected]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::unsigned_maximum(json!(u64::MAX))]
|
||||
#[case::above_maximum(json!(9_223_372_036_854_775_808_u64))]
|
||||
#[case::float_above_maximum(json!(9_223_372_036_854_775_808.0))]
|
||||
#[case::below_minimum(json!("-9223372036854775809"))]
|
||||
#[case::precise_fraction(json!("1.0000000000000001"))]
|
||||
#[case::exponent(json!("1e3"))]
|
||||
#[case::missing_fraction(json!("2."))]
|
||||
#[case::missing_integer(json!(".0"))]
|
||||
#[case::leading_separator(json!("_2"))]
|
||||
#[case::repeated_separator(json!("2__0"))]
|
||||
#[case::fraction(json!(2.5))]
|
||||
#[case::null(json!(null))]
|
||||
#[case::object(json!({}))]
|
||||
fn integers_reject_invalid_values(#[case] input: Value) {
|
||||
assert!(serde_json::from_value::<Numbers>(json!({"integers": [input]})).is_err());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::nan(json!("NaN"))]
|
||||
#[case::positive_infinity(json!("inf"))]
|
||||
#[case::negative_infinity(json!("-inf"))]
|
||||
#[case::overflow(json!("1e999"))]
|
||||
#[case::array(json!([]))]
|
||||
fn floats_reject_nonfinite_and_invalid_values(#[case] input: Value) {
|
||||
assert!(serde_json::from_value::<Numbers>(json!({"float": input})).is_err());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::integer(json!(2), 2.0)]
|
||||
#[case::float(json!(2.5), 2.5)]
|
||||
#[case::boolean(json!(true), 1.0)]
|
||||
fn floats_accept_finite_numbers(#[case] input: Value, #[case] expected: f64) {
|
||||
let numbers: Numbers = serde_json::from_value(json!({"float": input})).unwrap();
|
||||
assert_eq!(numbers.float, Some(expected));
|
||||
}
|
||||
32
litellm-rust/crates/llms-types/tests/wire_type.rs
Normal file
32
litellm-rust/crates/llms-types/tests/wire_type.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use litellm_llms_types::formats::chat_completions::ChatMessage;
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
|
||||
#[rstest]
|
||||
fn wire_type_preserves_serialization() {
|
||||
let message = ChatMessage {
|
||||
role: "user".to_owned(),
|
||||
content: None,
|
||||
name: None,
|
||||
extra: Default::default(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(message).unwrap(),
|
||||
json!({"role": "user"})
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
#[rstest]
|
||||
fn wire_type_supports_schema_generation() {
|
||||
let schema = schemars::schema_for!(ChatMessage);
|
||||
|
||||
assert!(
|
||||
schema
|
||||
.to_value()
|
||||
.get("properties")
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.is_some_and(|properties| properties.contains_key("role"))
|
||||
);
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ Use trait defaults for unchanged inherited behavior and explicit delegation for
|
|||
|
||||
Use named `#[rstest]` cases for independent input/output scenarios instead of loops or repeated calls in one test. Inject reusable setup with `#[fixture]` arguments and use `#[with(...)]` for fixture overrides. Keep assertions about the same result together
|
||||
|
||||
Base OCR currently keeps response models next to `BaseOcrConfig` in `src/base_llm/ocr/transformation.rs`. This is legacy placement, not an exception to the shared API contract ownership in `litellm-types`. Rust context/environment types support the runtime. `BaseOcrConfig::prepare_request` corresponds to Python's HTTP-handler preparation rather than a `BaseOCRConfig` method, and `validate_request_body` is a Rust-only hook. `src/base_llm/ocr/error.rs` and `src/base_llm/ocr/document.rs` are Rust-only: the OCR error taxonomy shared with the route, and inline-document helpers shared by several providers
|
||||
Shared OCR document and response contracts live in `litellm-llms-types::formats::ocr`. `BaseOcrConfig` and decoding into adapter errors remain in `src/base_llm/ocr/transformation.rs`. Rust context/environment types support the runtime. `BaseOcrConfig::prepare_request` corresponds to Python's HTTP-handler preparation rather than a `BaseOCRConfig` method, and `validate_request_body` is a Rust-only hook. `src/base_llm/ocr/error.rs` and `src/base_llm/ocr/document.rs` are Rust-only: the OCR error taxonomy shared with the route, and inline-document helpers shared by several providers
|
||||
|
||||
For Mistral, `async_transform_ocr_request` uses the base default in both languages. `resolve_headers` and `build_ocr_url` implement the respective environment and URL operations, and `normalize_response` implements the typed part of response transformation. Existing auth key/header handling and top-level response-extra preservation differ between languages; layout refactors must preserve those behaviors and verify them with the existing tests
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ Azure Messages maps to `llms/azure_ai/anthropic/messages_transformation.py`; Bed
|
|||
|
||||
## Provider and format boundaries
|
||||
|
||||
The same ownership rule applies to Messages, Responses, Chat Completions, OCR, and other API formats. `litellm-types` owns shared API data contracts. `llms/src/base_llm/<format>/` owns provider adapter contracts and shared transformation machinery. `llms/src/<provider>/<format>/` owns provider implementations and policy. `core/src/<format>/` owns call orchestration. Repeating a format name identifies the API each layer handles, not duplicate ownership of its schema. These boundaries also apply between modules in the same crate
|
||||
The same ownership rule applies to Messages, Responses, Chat Completions, OCR, and other API formats. `litellm-llms-types` owns shared API data contracts. `llms/src/base_llm/<format>/` owns provider adapter contracts and shared transformation machinery. `llms/src/<provider>/<format>/` owns provider implementations and policy. `core/src/<format>/` owns call orchestration. Repeating a format name identifies the API each layer handles, not duplicate ownership of its schema. These boundaries also apply between modules in the same crate
|
||||
|
||||
A provider adapter may explicitly reuse another provider's transformation helper when that policy applies to its backend, such as Bedrock's Claude adapter using Anthropic payload shaping. Reuse across hosts of the same model family does not make the policy format-wide. Keep provider policy out of shared trait defaults and generic normalization, and keep shared execution contexts limited to inputs the adapter contract actually needs. Pure payload rewrites belong with transformations, not transport handlers
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ repository.workspace = true
|
|||
test-support = ["litellm-http/test-support"]
|
||||
|
||||
[dependencies]
|
||||
litellm-types.workspace = true
|
||||
litellm-llms-types.workspace = true
|
||||
litellm-core-utils.workspace = true
|
||||
litellm-auth = { workspace = true, features = ["aws", "azure", "gcp"] }
|
||||
litellm-auth-aws.workspace = true
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
- Put behavior specific to the Messages API in `messages/`
|
||||
- Keep generic HTTP mechanics in `litellm-http`, configuration lookup in the existing settings utilities, and credential application in the shared auth layer
|
||||
- Choose authentication policy and required headers here, then let shared infrastructure apply those decisions
|
||||
- Consume shared API contracts from `litellm-types`. Do not define public Messages protocol types under this provider
|
||||
- Consume shared API contracts from `litellm-llms-types`. Do not define public Messages protocol types under this provider
|
||||
- Preserve Python's concepts and observable behavior where useful, without mechanically reproducing its class hierarchy, helpers, or file structure
|
||||
- `ReplayedWebSearchResult` and `ReplayedWebSearchContent` are private partial models for replay flattening, not complete public protocol contracts. Keep them private while they serve that transformation
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use litellm_types::llms::anthropic_messages::anthropic_response::AnthropicMessagesResponse;
|
||||
use litellm_llms_types::formats::batches::{BatchRequestCounts, BatchResponse, BatchStatus};
|
||||
use litellm_llms_types::formats::messages::MessagesResponse;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use time::OffsetDateTime;
|
||||
|
|
@ -45,46 +46,8 @@ struct BatchResultRecord {
|
|||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum BatchResult {
|
||||
Succeeded {
|
||||
message: Box<AnthropicMessagesResponse>,
|
||||
},
|
||||
Errored {
|
||||
error: Value,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BatchStatus {
|
||||
InProgress,
|
||||
Cancelling,
|
||||
Completed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BatchRequestCounts {
|
||||
pub total: u64,
|
||||
pub completed: u64,
|
||||
pub failed: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LiteLlmMessageBatch {
|
||||
pub id: String,
|
||||
pub object: String,
|
||||
pub endpoint: String,
|
||||
pub input_file_id: String,
|
||||
pub completion_window: String,
|
||||
pub status: BatchStatus,
|
||||
pub output_file_id: String,
|
||||
pub created_at: i64,
|
||||
pub in_progress_at: Option<i64>,
|
||||
pub expires_at: Option<i64>,
|
||||
pub completed_at: Option<i64>,
|
||||
pub expired_at: Option<i64>,
|
||||
pub cancelling_at: Option<i64>,
|
||||
pub cancelled_at: Option<i64>,
|
||||
pub request_counts: BatchRequestCounts,
|
||||
Succeeded { message: Box<MessagesResponse> },
|
||||
Errored { error: Value },
|
||||
}
|
||||
|
||||
pub trait AnthropicBatchesConfig {
|
||||
|
|
@ -100,7 +63,7 @@ pub trait AnthropicBatchesConfig {
|
|||
&self,
|
||||
response: AnthropicMessageBatch,
|
||||
now: i64,
|
||||
) -> Result<LiteLlmMessageBatch, Error>;
|
||||
) -> Result<BatchResponse, Error>;
|
||||
|
||||
fn retrieve_batch_url(
|
||||
&self,
|
||||
|
|
@ -115,9 +78,9 @@ pub trait AnthropicBatchesConfig {
|
|||
&self,
|
||||
response: AnthropicMessageBatch,
|
||||
now: i64,
|
||||
) -> LiteLlmMessageBatch;
|
||||
) -> BatchResponse;
|
||||
|
||||
fn transform_batch_results(&self, body: &str) -> Result<Vec<AnthropicMessagesResponse>, Error>;
|
||||
fn transform_batch_results(&self, body: &str) -> Result<Vec<MessagesResponse>, Error>;
|
||||
}
|
||||
|
||||
pub struct AnthropicBatchesTransformation;
|
||||
|
|
@ -172,7 +135,7 @@ impl AnthropicBatchesConfig for AnthropicBatchesTransformation {
|
|||
&self,
|
||||
_response: AnthropicMessageBatch,
|
||||
_now: i64,
|
||||
) -> Result<LiteLlmMessageBatch, Error> {
|
||||
) -> Result<BatchResponse, Error> {
|
||||
Err(Error::Unsupported("Anthropic message batch creation"))
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +163,7 @@ impl AnthropicBatchesConfig for AnthropicBatchesTransformation {
|
|||
&self,
|
||||
response: AnthropicMessageBatch,
|
||||
now: i64,
|
||||
) -> LiteLlmMessageBatch {
|
||||
) -> BatchResponse {
|
||||
let created_at = timestamp(response.created_at.as_deref());
|
||||
let ended_at = timestamp(response.ended_at.as_deref());
|
||||
let expires_at = timestamp(response.expires_at.as_deref());
|
||||
|
|
@ -221,7 +184,7 @@ impl AnthropicBatchesConfig for AnthropicBatchesTransformation {
|
|||
failed: response.request_counts.errored,
|
||||
};
|
||||
|
||||
LiteLlmMessageBatch {
|
||||
BatchResponse {
|
||||
id: response.id.clone(),
|
||||
object: "batch".into(),
|
||||
endpoint: "/v1/messages".into(),
|
||||
|
|
@ -248,7 +211,7 @@ impl AnthropicBatchesConfig for AnthropicBatchesTransformation {
|
|||
}
|
||||
}
|
||||
|
||||
fn transform_batch_results(&self, body: &str) -> Result<Vec<AnthropicMessagesResponse>, Error> {
|
||||
fn transform_batch_results(&self, body: &str) -> Result<Vec<MessagesResponse>, Error> {
|
||||
body.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.enumerate()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use litellm_types::messages::streaming::{
|
||||
MessagesContentBlock, MessagesContentBlockDelta, MessagesStreamEvent, MessagesStreamUsage,
|
||||
};
|
||||
use litellm_types::{
|
||||
llms::openai::{ChatCompletionThinkingBlock, ChatCompletionToolCallChunk},
|
||||
utils::{ChatCompletionChunk, ChatCompletionsUsage},
|
||||
use litellm_llms_types::formats::{
|
||||
chat_completions::{
|
||||
ChatCompletionChunk, ChatCompletionThinkingBlock, ChatCompletionToolCallChunk,
|
||||
ChatCompletionsUsage,
|
||||
},
|
||||
messages::streaming::{
|
||||
MessagesContentBlock, MessagesContentBlockDelta, MessagesStreamEvent, MessagesStreamUsage,
|
||||
},
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ use litellm_core_utils::{
|
|||
core_helpers::{finish_reason_for, unix_now, usage_from_parts},
|
||||
prompt_templates::factory::{Conversation, build_conversation},
|
||||
};
|
||||
use litellm_types::{
|
||||
llms::openai::ChatMessage,
|
||||
utils::{ChatCompletionsChoice, ChatCompletionsChoiceMessage, ChatCompletionsResponse},
|
||||
use litellm_llms_types::formats::chat_completions::{
|
||||
ChatCompletionsChoice, ChatCompletionsChoiceMessage, ChatCompletionsResponse, ChatMessage,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value, json};
|
||||
|
|
@ -50,16 +49,16 @@ const SUPPORTED_PARAMS: &[(&str, &str)] = &[
|
|||
];
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MessageResponse {
|
||||
struct TextResponseProjection {
|
||||
model: String,
|
||||
content: Vec<ContentBlock>,
|
||||
usage: MessageUsage,
|
||||
content: Vec<TextResponseBlock>,
|
||||
usage: ResponseUsageProjection,
|
||||
stop_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum ContentBlock {
|
||||
enum TextResponseBlock {
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
|
|
@ -68,7 +67,7 @@ enum ContentBlock {
|
|||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MessageUsage {
|
||||
struct ResponseUsageProjection {
|
||||
input_tokens: u64,
|
||||
output_tokens: u64,
|
||||
#[serde(default)]
|
||||
|
|
@ -124,16 +123,17 @@ impl BaseConfig for AnthropicConfig {
|
|||
_model: &str,
|
||||
response: ProviderChatResponseData,
|
||||
) -> Result<ChatCompletionsResponse, Error> {
|
||||
let body: MessageResponse = serde_json::from_value(response.body).map_err(|error| {
|
||||
Error::InvalidResponse(crate::ErrorDetail::invalid("messages response", error))
|
||||
})?;
|
||||
let body: TextResponseProjection =
|
||||
serde_json::from_value(response.body).map_err(|error| {
|
||||
Error::InvalidResponse(crate::ErrorDetail::invalid("messages response", error))
|
||||
})?;
|
||||
// The route declines tool and thinking requests, so a non-text block
|
||||
// means the response carries something this path never asked for.
|
||||
// Decline rather than silently dropping it; the host falls back.
|
||||
if body
|
||||
.content
|
||||
.iter()
|
||||
.any(|block| matches!(block, ContentBlock::Other))
|
||||
.any(|block| matches!(block, TextResponseBlock::Other))
|
||||
{
|
||||
return Err(Error::Unsupported("non-text response content block"));
|
||||
}
|
||||
|
|
@ -141,8 +141,8 @@ impl BaseConfig for AnthropicConfig {
|
|||
.content
|
||||
.into_iter()
|
||||
.map(|block| match block {
|
||||
ContentBlock::Text { text } => text,
|
||||
ContentBlock::Other => String::new(),
|
||||
TextResponseBlock::Text { text } => text,
|
||||
TextResponseBlock::Other => String::new(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue