mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-13 03:59:12 +00:00
* refactor(python-bridge): ship a signature base and read the resolved call NativeCall carries base (positionals by name plus signature defaults) instead of the fully bound dict. resolved lays kwargs over base, which is what bound held, so every pre-hook read and every route host keeps seeing the same values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(bedrock): build the transcription NativeCall with an empty base Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(dispatch): read the resolved call instead of bound Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(host-python): rename effective to effective_py_args and note the shallow copy Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
317 lines
12 KiB
Python
317 lines
12 KiB
Python
"""The native Messages route shapes the wire request the way the Python handler does.
|
|
|
|
Model capability expectations come from model_prices_and_context_window.json (Claude Sonnet 5 is an
|
|
adaptive-thinking model without sampling params; Claude Haiku 4.5 is a legacy-thinking model), read at
|
|
2026-09-24; the cost map is LiteLLM's own file.
|
|
"""
|
|
|
|
from collections.abc import Iterator
|
|
from typing import Final
|
|
|
|
import pytest
|
|
|
|
import litellm
|
|
from litellm.rust_bridge import catalog
|
|
from litellm.rust_bridge.catalog import Route, RouteRule
|
|
from litellm.rust_bridge.configuration import Rollout
|
|
from tests.test_litellm_rust.support.isolation import rebound
|
|
from tests.test_litellm_rust.support.recording_server import RecordingServer, ResponseSpec
|
|
from tests.test_litellm_rust.support.requests import MESSAGES, MESSAGES_RESPONSE
|
|
|
|
pytestmark = pytest.mark.requires_rust_extension
|
|
|
|
ADAPTIVE_MODEL: Final = "anthropic/claude-sonnet-5"
|
|
LEGACY_THINKING_MODEL: Final = "anthropic/claude-haiku-4-5"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def opt_messages_into_rust() -> Iterator[None]:
|
|
with rebound(catalog, "RULES", (RouteRule(Route.MESSAGES, Rollout.RUST_OPT_IN), *catalog.RULES)):
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def messages_server(recording_server: RecordingServer) -> RecordingServer:
|
|
recording_server.default_response = ResponseSpec(body=MESSAGES_RESPONSE)
|
|
return recording_server
|
|
|
|
|
|
def arguments(server: RecordingServer, **kwargs: object) -> dict[str, object]:
|
|
return {
|
|
"model": ADAPTIVE_MODEL,
|
|
"messages": [dict(message) for message in MESSAGES],
|
|
"max_tokens": 8192,
|
|
"api_key": "test-key",
|
|
"api_base": server.base_url,
|
|
**kwargs,
|
|
}
|
|
|
|
|
|
def sent(server: RecordingServer) -> tuple[dict[str, object], dict[str, str]]:
|
|
assert len(server.requests) == 1
|
|
request: Final = server.requests[0]
|
|
assert not request.headers.get("user-agent", "").startswith("python-httpx")
|
|
assert isinstance(request.body, dict)
|
|
return request.body, request.headers
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reasoning_effort_becomes_adaptive_thinking_and_effort_on_the_wire(
|
|
messages_server: RecordingServer,
|
|
) -> None:
|
|
await litellm.anthropic.messages.acreate(**arguments(messages_server, reasoning_effort="high"))
|
|
|
|
body, _ = sent(messages_server)
|
|
assert "reasoning_effort" not in body
|
|
assert body["thinking"] == {"type": "adaptive", "display": "summarized"}
|
|
assert body["output_config"] == {"effort": "high"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_claude_code_adaptive_payload_is_downgraded_to_a_capped_budget_for_a_legacy_model(
|
|
messages_server: RecordingServer,
|
|
) -> None:
|
|
await litellm.anthropic.messages.acreate(
|
|
**arguments(
|
|
messages_server,
|
|
model=LEGACY_THINKING_MODEL,
|
|
max_tokens=3000,
|
|
thinking={"type": "adaptive"},
|
|
output_config={"effort": "high"},
|
|
temperature=0,
|
|
)
|
|
)
|
|
|
|
body, _ = sent(messages_server)
|
|
assert body["thinking"] == {"type": "enabled", "budget_tokens": 2999}
|
|
assert "output_config" not in body
|
|
assert "temperature" not in body
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_removed_sampling_params_are_dropped_under_drop_params(messages_server: RecordingServer) -> None:
|
|
await litellm.anthropic.messages.acreate(
|
|
**arguments(messages_server, temperature=0.2, top_p=0.9, top_k=5, drop_params=True)
|
|
)
|
|
|
|
body, _ = sent(messages_server)
|
|
assert not {"temperature", "top_p", "top_k"} & body.keys()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_removed_sampling_params_are_rejected_without_drop_params(messages_server: RecordingServer) -> None:
|
|
messages_server.expected_requests = 0
|
|
|
|
with pytest.raises(litellm.BadRequestError, match="does not support top_k=5"):
|
|
await litellm.anthropic.messages.acreate(**arguments(messages_server, top_k=5))
|
|
|
|
assert messages_server.requests == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_replayed_history_is_sanitized_before_it_reaches_the_provider(
|
|
messages_server: RecordingServer,
|
|
) -> None:
|
|
history: Final = [
|
|
{"role": "user", "content": "run it"},
|
|
{
|
|
"role": "assistant",
|
|
"content": [
|
|
{"type": "text", "text": ""},
|
|
{
|
|
"type": "tool_use",
|
|
"id": "functions.Bash:0",
|
|
"name": "Bash",
|
|
"input": {},
|
|
"provider_specific_fields": {"x": 1},
|
|
},
|
|
],
|
|
},
|
|
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "functions.Bash:0", "content": "ok"}]},
|
|
]
|
|
|
|
await litellm.anthropic.messages.acreate(**arguments(messages_server, messages=history))
|
|
|
|
body, _ = sent(messages_server)
|
|
assert body["messages"] == [
|
|
{"role": "user", "content": "run it"},
|
|
{"role": "assistant", "content": [{"type": "tool_use", "id": "functions_Bash_0", "name": "Bash", "input": {}}]},
|
|
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "functions_Bash_0", "content": "ok"}]},
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_feature_betas_merge_into_the_forwarded_beta_header(messages_server: RecordingServer) -> None:
|
|
await litellm.anthropic.messages.acreate(
|
|
**arguments(
|
|
messages_server,
|
|
output_format={"type": "json_schema", "schema": {"type": "object"}},
|
|
extra_headers={"anthropic-beta": "web-search-2025-03-05"},
|
|
)
|
|
)
|
|
|
|
_, headers = sent(messages_server)
|
|
assert headers["anthropic-beta"] == "structured-outputs-2025-11-13,web-search-2025-03-05"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_oauth_token_authenticates_as_a_bearer_with_the_oauth_beta(messages_server: RecordingServer) -> None:
|
|
await litellm.anthropic.messages.acreate(**arguments(messages_server, api_key="sk-ant-oat01-token"))
|
|
|
|
_, headers = sent(messages_server)
|
|
assert "x-api-key" not in headers
|
|
assert headers["authorization"] == "Bearer sk-ant-oat01-token"
|
|
assert headers["anthropic-beta"] == "oauth-2025-04-20"
|
|
assert headers["anthropic-dangerous-direct-browser-access"] == "true"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_metadata_is_reduced_to_the_fields_anthropic_accepts(messages_server: RecordingServer) -> None:
|
|
await litellm.anthropic.messages.acreate(
|
|
**arguments(messages_server, metadata={"user_id": "u-1", "trace_id": "internal"})
|
|
)
|
|
|
|
body, _ = sent(messages_server)
|
|
assert body["metadata"] == {"user_id": "u-1"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_additional_drop_params_remove_nested_fields_from_the_wire(messages_server: RecordingServer) -> None:
|
|
tools: Final = [{"name": "lookup", "input_schema": {"type": "object"}, "input_examples": [{"q": "x"}]}]
|
|
|
|
await litellm.anthropic.messages.acreate(
|
|
**arguments(messages_server, tools=tools, additional_drop_params=["tools[*].input_examples"])
|
|
)
|
|
|
|
body, _ = sent(messages_server)
|
|
assert body["tools"] == [{"name": "lookup", "input_schema": {"type": "object"}}]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_provider_specific_headers_scoped_to_anthropic_reach_the_wire(messages_server: RecordingServer) -> None:
|
|
await litellm.anthropic.messages.acreate(
|
|
**arguments(
|
|
messages_server,
|
|
provider_specific_header=[
|
|
{"custom_llm_provider": "anthropic, azure_ai", "extra_headers": {"x-scoped": "yes"}},
|
|
{"custom_llm_provider": "openai", "extra_headers": {"x-other": "no"}},
|
|
],
|
|
)
|
|
)
|
|
|
|
_, headers = sent(messages_server)
|
|
assert headers["x-scoped"] == "yes"
|
|
assert "x-other" not in headers
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scoped_headers_override_extra_headers_which_override_forwarded_headers(
|
|
messages_server: RecordingServer,
|
|
) -> None:
|
|
await litellm.anthropic.messages.acreate(
|
|
**arguments(
|
|
messages_server,
|
|
headers={"x-priority": "forwarded", "x-forwarded-only": "kept"},
|
|
extra_headers={"x-priority": "extra", "x-extra-only": "kept"},
|
|
provider_specific_header={"custom_llm_provider": "anthropic", "extra_headers": {"x-priority": "scoped"}},
|
|
)
|
|
)
|
|
|
|
_, headers = sent(messages_server)
|
|
assert {name: headers.get(name) for name in ("x-priority", "x-forwarded-only", "x-extra-only")} == {
|
|
"x-priority": "scoped",
|
|
"x-forwarded-only": "kept",
|
|
"x-extra-only": "kept",
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_non_string_metadata_user_id_is_rejected_before_the_provider_call(
|
|
messages_server: RecordingServer,
|
|
) -> None:
|
|
messages_server.expected_requests = 0
|
|
|
|
with pytest.raises(litellm.BadRequestError, match=r"metadata\.user_id must be a string"):
|
|
await litellm.anthropic.messages.acreate(**arguments(messages_server, metadata={"user_id": 123}))
|
|
|
|
assert messages_server.requests == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_native_messages_observes_runtime_capabilities_and_separate_caller_settings(
|
|
messages_server: RecordingServer, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
from litellm.rust_bridge.messages.entrypoints import NATIVE_AMESSAGES
|
|
from litellm.rust_bridge.public_call import NativeCall
|
|
|
|
native: Final = NATIVE_AMESSAGES.load()
|
|
assert native is not None
|
|
model: Final = "claude-test-runtime-capabilities"
|
|
messages_server.expected_requests = 2
|
|
request: Final = NativeCall(
|
|
args=(),
|
|
kwargs={"temperature": 0.2, "drop_params": True},
|
|
base={
|
|
"model": model,
|
|
"messages": MESSAGES,
|
|
"max_tokens": 16,
|
|
"stream": None,
|
|
"api_key": "test-key",
|
|
"api_base": messages_server.base_url,
|
|
"custom_llm_provider": "anthropic",
|
|
"temperature": 0.2,
|
|
"drop_params": True,
|
|
},
|
|
)
|
|
monkeypatch.setitem(
|
|
litellm.model_cost,
|
|
model,
|
|
{
|
|
"litellm_provider": "anthropic",
|
|
"mode": "chat",
|
|
"supports_sampling_params": True,
|
|
},
|
|
)
|
|
first: Final = await native(request)
|
|
monkeypatch.setitem(
|
|
litellm.model_cost,
|
|
model,
|
|
{
|
|
"litellm_provider": "anthropic",
|
|
"mode": "chat",
|
|
"supports_sampling_params": False,
|
|
},
|
|
)
|
|
second: Final = await native(request)
|
|
|
|
assert isinstance(first, dict)
|
|
assert isinstance(second, dict)
|
|
assert first["id"] == second["id"] == MESSAGES_RESPONSE["id"]
|
|
assert len(messages_server.requests) == 2
|
|
first_body: Final = messages_server.requests[0].body
|
|
second_body: Final = messages_server.requests[1].body
|
|
assert isinstance(first_body, dict)
|
|
assert isinstance(second_body, dict)
|
|
assert first_body["temperature"] == request.kwargs["temperature"]
|
|
assert second_body == {name: value for name, value in first_body.items() if name != "temperature"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_native_messages_reads_optional_positional_body_parameters(messages_server: RecordingServer) -> None:
|
|
from litellm.messages.dispatch import _MESSAGES, _public_request
|
|
from litellm.rust_bridge.messages.entrypoints import NATIVE_AMESSAGES
|
|
|
|
native: Final = NATIVE_AMESSAGES.load()
|
|
assert native is not None
|
|
metadata: Final = {"user_id": "caller"}
|
|
args: Final = (16, MESSAGES, "anthropic/claude-test", metadata, None, False, "Be brief", 0.25)
|
|
kwargs: Final = {"api_key": "test-key", "api_base": messages_server.base_url}
|
|
call: Final = _public_request(_MESSAGES, args, kwargs)
|
|
assert call is not None
|
|
|
|
await native(call)
|
|
|
|
body, _ = sent(messages_server)
|
|
assert body["temperature"] == args[7]
|
|
assert body["system"] == args[6]
|
|
assert body["metadata"] == metadata
|