This commit is contained in:
Lujaec 2026-10-31 15:49:20 +09:00 • committed by GitHub
commit 92caf6b0f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 83 additions and 2 deletions

View file

@ -417,6 +417,10 @@ DROP_SERVICE_TIER_PROVIDER_WARNING: Final = (
"Dropping `service_tier` for model=%s on provider=%s. Only the Anthropic API routes accept it."
)
DROP_THREAD_PROVIDER_WARNING: Final = (
"Dropping `thread` for model=%s on provider=%s. Only the Anthropic API keeps Message Threads."
)
DROP_UNSUPPORTED_TOOL_SEARCH_WARNING: Final = (
"Dropping the OpenAI `tool_search` tool and the `defer_loading` of the other tools for model=%s on %s "
"(drop_params=True) because this route cannot run that tool search."
@ -698,6 +702,17 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
litellm.verbose_logger.warning(DROP_SERVICE_TIER_PROVIDER_WARNING, model, custom_llm_provider)
optional_params.pop("service_tier", None)
@staticmethod
def maybe_drop_thread_param(
model: str,
optional_params: dict[str, object], # mutable-ok: mirrors override contract
custom_llm_provider: str | None = None,
) -> None:
if "thread" not in optional_params or custom_llm_provider in (None, "anthropic"):
return
litellm.verbose_logger.warning(DROP_THREAD_PROVIDER_WARNING, model, custom_llm_provider)
optional_params.pop("thread", None)
@staticmethod
def _raise_invalid_reasoning_effort(model: str, value: object, llm_provider: str) -> NoReturn:
"""Raise a ``BadRequestError`` for an unrecognised ``reasoning_effort``.

View file

@ -288,7 +288,7 @@ class AnthropicAdapter:
#########################################################
# Created Typed Request Body
#########################################################
request_body: Final = AnthropicMessagesRequest(model=model, messages=messages, **kwargs)
request_body: Final = AnthropicMessagesRequest(model=model, messages=messages, **kwargs) # pyright: ignore[reportUnknownArgumentType] # the request params arrive as a bare dict
(
translated_body,

View file

@ -793,7 +793,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
),
max_tokens=max_tokens,
model=model,
**anthropic_messages_optional_request_params,
**anthropic_messages_optional_request_params, # pyright: ignore[reportUnknownArgumentType] # the request params arrive as a bare dict
)
request: Final = (
{**dict(anthropic_messages_request), "thinking": gated.thinking}

View file

@ -301,6 +301,11 @@ class AnthropicMessagesRequestUtils:
optional_params=filtered_params,
custom_llm_provider=custom_llm_provider,
)
AnthropicConfig.maybe_drop_thread_param(
model=model,
optional_params=filtered_params,
custom_llm_provider=custom_llm_provider,
)
for param in ("temperature", "top_p", "top_k"):
if param in filtered_params:
AnthropicModelInfo._apply_sampling_param( # pyright: ignore[reportPrivateUsage] # same gating the /chat/completions path applies; forking it would drift

View file

@ -440,6 +440,7 @@ class AnthropicMessagesRequestOptionalParams(TypedDict, total=False):
reasoning_effort: str | None
safeguards: ReadOnly[list[dict[str, object]] | None]
service_tier: ReadOnly[str | None]
thread: ReadOnly[Mapping[str, object] | None]
class AnthropicMessagesRequest(AnthropicMessagesRequestOptionalParams, total=False):

View file

@ -1633,6 +1633,66 @@ async def test_anthropic_messages_forwards_safeguards_and_unknown_beta_to_anthro
assert response["safeguard_results"] == safeguard_results
@pytest.mark.asyncio
async def test_anthropic_messages_forwards_message_threads_continue_to_anthropic():
"""Shape is what Claude Code 2.1.312 sends with the message-threads-2026-08-12 beta, captured 2026-10-29."""
from litellm.llms.anthropic.messages.native import handler
thread = {"type": "continue", "previous_message_id": "msg_01ABC"}
tool_result_only = [
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_01", "content": "README contents"}]}
]
captured: dict[str, object] = {}
def upstream_records_the_request(request: httpx.Request) -> httpx.Response:
captured["body"] = json.loads(request.content)
return httpx.Response(
200,
json={
"id": "msg_02DEF",
"type": "message",
"role": "assistant",
"model": "claude-haiku-4-5",
"content": [{"type": "text", "text": "ok"}],
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {"input_tokens": 1, "output_tokens": 1},
},
request=request,
)
upstream = AsyncHTTPHandler()
upstream.client = httpx.AsyncClient(transport=httpx.MockTransport(upstream_records_the_request))
await handler.anthropic_messages(
max_tokens=16,
messages=tool_result_only,
model="anthropic/claude-haiku-4-5",
custom_llm_provider="anthropic",
api_key="sk-test",
client=upstream,
thread=thread,
extra_headers={"anthropic-beta": "message-threads-2026-08-12"},
)
assert captured["body"]["thread"] == thread
assert captured["body"]["messages"] == tool_result_only
@pytest.mark.parametrize("custom_llm_provider", ["vertex_ai", "bedrock", "azure_ai", "databricks"])
def test_anthropic_messages_drops_thread_where_message_threads_are_not_served(custom_llm_provider: str) -> None:
from litellm.llms.anthropic.messages.native.utils import AnthropicMessagesRequestUtils
optional_params = AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param(
params={"max_tokens": 64, "thread": {"type": "create"}},
model="claude-haiku-4-5",
custom_llm_provider=custom_llm_provider,
)
assert "thread" not in optional_params
assert dict(optional_params).get("max_tokens") == 64
@pytest.mark.asyncio
async def test_anthropic_messages_streaming_forwards_safeguards_and_keeps_safeguard_results():
"""Shapes are what Claude Code 2.1.278 sends and api.anthropic.com returns, captured 2026-09-21."""