fix(anthropic): answer an unbound thread id on chat and Responses with the 404 and drop the stale beta-join tests

This commit is contained in:
mateo-berri 2026-11-04 22:25:01 -08:00
parent 7b5ea297e0
commit 5df3df7fa6
4 changed files with 11 additions and 63 deletions

View file

@ -20,7 +20,6 @@ if TYPE_CHECKING:
from litellm.proxy.utils import InternalUsageCache
_PASS_THROUGH_CALL_TYPE: Final = "pass_through_endpoint"
_MESSAGES_CALL_TYPE: Final = "anthropic_messages"
_ANTHROPIC_BETA_HEADER: Final = "anthropic-beta"
_OWNER_KEY_PREFIX: Final = "anthropic_message_thread_owner:"
_OWNER_TTL_SECONDS: Final = 3600
@ -217,7 +216,7 @@ class MessageThreadOwnership(CustomLogger):
if previous_message_id is None:
return None
binding: Final = await self._binding(previous_message_id, user_api_key_dict)
if binding is None and call_type != _MESSAGES_CALL_TYPE:
if binding is None and call_type == _PASS_THROUGH_CALL_TYPE:
return None
if binding is None or binding.owner != _owner(user_api_key_dict):
raise HTTPException(status_code=404, detail=message_thread_not_found_body(_UNOWNED_THREAD_MESSAGE))

View file

@ -131,15 +131,19 @@ async def test_without_a_key_only_the_signed_in_identity_that_created_a_thread_c
assert not await allows(ownership, bob, "msg_signed_in")
@pytest.mark.parametrize("call_type", ("anthropic_messages", "acompletion", "aresponses"))
@pytest.mark.asyncio
async def test_a_continue_naming_a_message_this_proxy_never_served_in_a_thread_is_thread_not_found() -> None:
async def test_a_continue_naming_a_message_this_proxy_never_served_in_a_thread_is_thread_not_found(
call_type: CallTypesLiteral,
) -> None:
ownership: Final = hook()
await ownership.async_post_call_success_hook(
data={}, user_api_key_dict=OWNER, response=anthropic_message("msg_unthreaded")
)
assert not await allows(ownership, OWNER, "msg_unthreaded")
assert not await allows(ownership, OWNER, "msg_unknown")
assert not await allows(ownership, OWNER, "msg_unthreaded", call_type)
assert not await allows(ownership, OWNER, "msg_unknown", call_type)
assert not await allows(ownership, INTRUDER, "msg_unknown", call_type)
@pytest.mark.asyncio
@ -182,13 +186,10 @@ async def test_a_thread_continue_on_another_route_stays_with_the_key_that_create
assert not await allows(ownership, INTRUDER, "msg_cross_route", call_type)
@pytest.mark.parametrize("call_type", ("pass_through_endpoint", "acompletion", "aresponses"))
@pytest.mark.asyncio
async def test_a_continue_outside_the_messages_route_of_a_message_the_proxy_never_bound_is_left_to_the_upstream(
call_type: CallTypesLiteral,
) -> None:
assert await allows(hook(), OWNER, "msg_unknown", call_type)
assert await allows(hook(), INTRUDER, "msg_unknown", call_type)
async def test_a_pass_through_continue_of_a_message_the_proxy_never_bound_is_left_to_the_upstream() -> None:
assert await allows(hook(), OWNER, "msg_unknown", "pass_through_endpoint")
assert await allows(hook(), INTRUDER, "msg_unknown", "pass_through_endpoint")
@pytest.mark.asyncio

View file

@ -968,23 +968,6 @@ class TestMergeProviderSpecificHeaders:
extra_headers=None, provider_specific_header=None, custom_llm_provider="chatgpt"
) is None
def test_client_anthropic_betas_join_the_extra_header_betas(self):
merged = ResponsesAPIRequestUtils.merge_provider_specific_headers(
extra_headers={"anthropic-beta": "advisor-tool-2026-03-01", "x-tenant": "team-a"},
provider_specific_header={
"custom_llm_provider": "anthropic",
"extra_headers": {"anthropic-beta": "message-threads-2026-08-12"},
},
custom_llm_provider="anthropic",
)
assert merged is not None
assert merged["x-tenant"] == "team-a"
assert set(str(merged["anthropic-beta"]).split(",")) == {
"advisor-tool-2026-03-01",
"message-threads-2026-08-12",
}
def test_responses_object_bills_an_explicit_cache_hit_at_the_explicit_rate():
model = "dashscope/qwen-responses-explicit-cache-test"

View file

@ -5384,41 +5384,6 @@ def test_anthropic_oauth_credential_does_not_persist_into_next_provider_hop():
assert "anthropic-version" not in shared_headers
def test_anthropic_chat_sends_the_client_betas_beside_the_extra_header_betas():
from litellm.types.utils import ProviderSpecificHeader
def echo_sent_betas(request: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
json={
"id": "msg_01EchoedBetas",
"type": "message",
"role": "assistant",
"model": "claude-haiku-5-5",
"content": [{"type": "text", "text": request.headers.get("anthropic-beta", "")}],
"stop_reason": "end_turn",
"usage": {"input_tokens": 1, "output_tokens": 1},
},
)
client: Final = HTTPHandler()
client.client = httpx.Client(transport=httpx.MockTransport(echo_sent_betas))
response: Final = litellm.completion(
model="anthropic/claude-haiku-5-5",
messages=[{"role": "user", "content": "Say OK"}],
extra_headers={"anthropic-beta": "advisor-tool-2026-03-01"},
provider_specific_header=ProviderSpecificHeader(
custom_llm_provider="anthropic", extra_headers={"anthropic-beta": "message-threads-2026-08-12"}
),
api_key="sk-fake-anthropic-key",
client=client,
)
sent_betas: Final = frozenset(beta.strip() for beta in response.choices[0].message.content.split(","))
assert {"advisor-tool-2026-03-01", "message-threads-2026-08-12"} <= sent_betas
STREAM_COST_MODEL = "gpt-4o"
STREAMED_USAGE = {"prompt_tokens": 137, "completion_tokens": 42, "total_tokens": 179}