mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-29 01:42:19 +00:00
fix(advisor): use model metadata for native advisor routing
Some checks are pending
Unit Tests: Proxy DB Operations / proxy-db (auth-checks, tests/proxy_unit_tests/test_auth_checks.py tests/proxy_unit_tests/test_user_api_key_auth.py, 20, 8) (push) Waiting to run
Unit Tests: Proxy DB Operations / proxy-db (key-generation, tests/proxy_unit_tests/test_key_generate_prisma.py, 30, 0) (push) Waiting to run
Unit Tests: Proxy DB Operations / proxy-db (remaining, tests/proxy_unit_tests --ignore=tests/proxy_unit_tests/test_key_generate_prisma.py --ignore=tests/proxy_unit_tests/test_auth_checks.py --ignore=tests/proxy_unit_tests/test_user_api_key_auth.py, 30, 8) (push) Waiting to run
Unit Tests: Security / security (push) Waiting to run
Some checks are pending
Unit Tests: Proxy DB Operations / proxy-db (auth-checks, tests/proxy_unit_tests/test_auth_checks.py tests/proxy_unit_tests/test_user_api_key_auth.py, 20, 8) (push) Waiting to run
Unit Tests: Proxy DB Operations / proxy-db (key-generation, tests/proxy_unit_tests/test_key_generate_prisma.py, 30, 0) (push) Waiting to run
Unit Tests: Proxy DB Operations / proxy-db (remaining, tests/proxy_unit_tests --ignore=tests/proxy_unit_tests/test_key_generate_prisma.py --ignore=tests/proxy_unit_tests/test_auth_checks.py --ignore=tests/proxy_unit_tests/test_user_api_key_auth.py, 30, 8) (push) Waiting to run
Unit Tests: Security / security (push) Waiting to run
Replace hardcoded Opus checks with a model-capability flag and shared alias resolution, ensure pre-request hooks still run for intercepted /messages calls, and clean up advisor stream tracking on failure paths. Made-with: Cursor
This commit is contained in:
parent
2a198265ac
commit
54fb9da4b2
11 changed files with 251 additions and 123 deletions
|
|
@ -1904,6 +1904,7 @@ if TYPE_CHECKING:
|
|||
supports_audio_output: Callable[..., bool]
|
||||
supports_system_messages: Callable[..., bool]
|
||||
supports_reasoning: Callable[..., bool]
|
||||
supports_native_advisor_tool: Callable[..., bool]
|
||||
acreate: Callable[..., Any]
|
||||
get_max_tokens: Callable[..., int]
|
||||
get_model_info: Callable[..., _ModelInfoType] # type: ignore[no-redef]
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ from litellm.integrations.advisor_interception.tools import (
|
|||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.types.integrations.advisor_interception import AdvisorInterceptionConfig
|
||||
from litellm.types.utils import CallTypes, LlmProviders
|
||||
from litellm.utils import (
|
||||
resolve_proxy_model_alias_to_litellm_model,
|
||||
supports_native_advisor_tool,
|
||||
)
|
||||
|
||||
|
||||
class AdvisorInterceptionLogger(CustomLogger):
|
||||
|
|
@ -224,6 +228,18 @@ class AdvisorInterceptionLogger(CustomLogger):
|
|||
return self._wrap_as_streaming_if_needed(result)
|
||||
return result
|
||||
|
||||
async def async_log_failure_event(
|
||||
self, kwargs, response_obj, start_time, end_time
|
||||
) -> None:
|
||||
"""
|
||||
Cleanup per-call advisor state on failure paths.
|
||||
"""
|
||||
call_id = kwargs.get("litellm_call_id")
|
||||
if isinstance(call_id, str):
|
||||
self._advisor_config_by_call_id.pop(call_id, None)
|
||||
self._skip_post_hook_call_ids.discard(call_id)
|
||||
self._converted_stream_call_ids.discard(call_id)
|
||||
|
||||
async def async_should_run_chat_completion_agentic_loop(
|
||||
self,
|
||||
response: Any,
|
||||
|
|
@ -244,7 +260,7 @@ class AdvisorInterceptionLogger(CustomLogger):
|
|||
return False, {}
|
||||
|
||||
# Only skip the orchestration loop for native providers when the advisor
|
||||
# model is actually supported natively (Anthropic Claude Opus 4.6).
|
||||
# model is actually supported natively by the provider.
|
||||
# For Anthropic executors with a non-native advisor model, fall through
|
||||
# to the orchestration loop below.
|
||||
if custom_llm_provider in ADVISOR_NATIVE_PROVIDERS:
|
||||
|
|
@ -422,6 +438,8 @@ class AdvisorInterceptionLogger(CustomLogger):
|
|||
finally:
|
||||
if isinstance(call_id, str):
|
||||
self._advisor_config_by_call_id.pop(call_id, None)
|
||||
self._skip_post_hook_call_ids.discard(call_id)
|
||||
self._converted_stream_call_ids.discard(call_id)
|
||||
|
||||
@staticmethod
|
||||
def _wrap_as_streaming_if_needed(response: Any) -> Any:
|
||||
|
|
@ -456,33 +474,16 @@ class AdvisorInterceptionLogger(CustomLogger):
|
|||
@staticmethod
|
||||
def _is_native_anthropic_advisor_model(advisor_model: str) -> bool:
|
||||
"""
|
||||
Return True only when the advisor model resolves to Anthropic Claude Opus 4.6,
|
||||
which is the only model Anthropic supports as a native advisor.
|
||||
Return True when the advisor model supports Anthropic native advisor.
|
||||
|
||||
Handles bare model names, litellm provider-prefixed names
|
||||
(e.g. ``anthropic/claude-opus-4-6``) and proxy model aliases.
|
||||
"""
|
||||
# Resolve proxy alias → underlying litellm model string first.
|
||||
try:
|
||||
llm_router = AdvisorInterceptionLogger._get_llm_router()
|
||||
if llm_router is not None:
|
||||
for deployment in llm_router.model_list or []:
|
||||
if deployment.get("model_name") == advisor_model:
|
||||
advisor_model = (
|
||||
deployment.get("litellm_params", {}).get("model")
|
||||
or advisor_model
|
||||
)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
normalized = advisor_model.lower().replace("_", "-")
|
||||
# Must be an Anthropic model and specifically opus-4-6.
|
||||
is_anthropic = normalized.startswith("anthropic/") or (
|
||||
"/" not in normalized and "claude" in normalized
|
||||
resolved_model = resolve_proxy_model_alias_to_litellm_model(advisor_model)
|
||||
model_to_check = resolved_model or advisor_model
|
||||
return supports_native_advisor_tool(
|
||||
model=model_to_check, custom_llm_provider="anthropic"
|
||||
)
|
||||
is_opus_46 = "claude-opus-4-6" in normalized or "claude-opus-4.6" in normalized
|
||||
return is_anthropic and is_opus_46
|
||||
|
||||
@staticmethod
|
||||
async def _call_advisor_model(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import asyncio
|
||||
import contextvars
|
||||
from functools import partial
|
||||
from typing import Any, AsyncIterator, Coroutine, Dict, List, Optional, Union, cast
|
||||
from typing import Any, AsyncIterator, Coroutine, Dict, List, Optional, Tuple, Type, Union, cast
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
|
|
@ -57,6 +57,7 @@ async def _execute_pre_request_hooks(
|
|||
tools: Optional[List[Dict]],
|
||||
stream: Optional[bool],
|
||||
custom_llm_provider: Optional[str],
|
||||
skip_callback_types: Optional[Tuple[Type[Any], ...]] = None,
|
||||
**kwargs,
|
||||
) -> Dict:
|
||||
"""
|
||||
|
|
@ -86,6 +87,7 @@ async def _execute_pre_request_hooks(
|
|||
|
||||
# Build complete request kwargs dict
|
||||
request_kwargs = {
|
||||
"messages": messages,
|
||||
"tools": tools,
|
||||
"stream": stream,
|
||||
"litellm_params": {
|
||||
|
|
@ -102,6 +104,8 @@ async def _execute_pre_request_hooks(
|
|||
for callback in litellm.callbacks:
|
||||
if not isinstance(callback, _CustomLogger):
|
||||
continue
|
||||
if skip_callback_types and isinstance(callback, skip_callback_types):
|
||||
continue
|
||||
|
||||
# Call the pre-request hook
|
||||
modified_kwargs = await callback.async_pre_request_hook(
|
||||
|
|
@ -199,10 +203,43 @@ async def anthropic_messages(
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# Execute pre-request hooks first so user CustomLoggers can mutate request
|
||||
# params even when a MessagesInterceptor handles the call. Skip only the
|
||||
# advisor interception hook here so advisor tools remain visible.
|
||||
from litellm.integrations.advisor_interception.handler import (
|
||||
AdvisorInterceptionLogger,
|
||||
)
|
||||
|
||||
request_kwargs = await _execute_pre_request_hooks(
|
||||
model=model,
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
stream=stream,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
skip_callback_types=(AdvisorInterceptionLogger,),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Extract modified parameters
|
||||
messages = request_kwargs.pop("messages", messages)
|
||||
tools = request_kwargs.pop("tools", tools)
|
||||
stream = request_kwargs.pop("stream", stream)
|
||||
# Propagate provider derived in pre-request hooks.
|
||||
if not custom_llm_provider:
|
||||
custom_llm_provider = request_kwargs.get("litellm_params", {}).get(
|
||||
"custom_llm_provider"
|
||||
)
|
||||
if not custom_llm_provider:
|
||||
try:
|
||||
_, custom_llm_provider, _, _ = litellm.get_llm_provider(model=model)
|
||||
except Exception:
|
||||
pass
|
||||
request_kwargs.pop("litellm_params", None)
|
||||
kwargs.update(request_kwargs)
|
||||
|
||||
# Run registered MessagesInterceptors (e.g. advisor orchestration loop)
|
||||
# BEFORE pre-request hooks, because hooks like AdvisorInterceptionLogger
|
||||
# would convert the advisor_20260301 tool into an OpenAI function tool,
|
||||
# making it invisible to the Messages API interceptor.
|
||||
# after non-advisor pre-request hooks have had a chance to mutate request
|
||||
# data, while keeping advisor tools unconverted for visibility.
|
||||
for interceptor in get_messages_interceptors():
|
||||
if interceptor.can_handle(tools, custom_llm_provider):
|
||||
return await interceptor.handle(
|
||||
|
|
@ -217,34 +254,6 @@ async def anthropic_messages(
|
|||
**kwargs,
|
||||
)
|
||||
|
||||
# Execute pre-request hooks to allow CustomLoggers to modify request
|
||||
request_kwargs = await _execute_pre_request_hooks(
|
||||
model=model,
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
stream=stream,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Extract modified parameters
|
||||
tools = request_kwargs.pop("tools", tools)
|
||||
stream = request_kwargs.pop("stream", stream)
|
||||
# Propagate the provider derived inside pre-request hooks, if not already set.
|
||||
if not custom_llm_provider:
|
||||
custom_llm_provider = request_kwargs.get("litellm_params", {}).get(
|
||||
"custom_llm_provider"
|
||||
)
|
||||
if not custom_llm_provider:
|
||||
try:
|
||||
_, custom_llm_provider, _, _ = litellm.get_llm_provider(model=model)
|
||||
except Exception:
|
||||
pass
|
||||
# Remove litellm_params from kwargs (only needed for hooks)
|
||||
request_kwargs.pop("litellm_params", None)
|
||||
# Merge back any other modifications
|
||||
kwargs.update(request_kwargs)
|
||||
|
||||
# Short-circuit web-search-only requests: detect the pattern, execute
|
||||
# search directly via Tavily/Perplexity, and return a synthetic response
|
||||
# without ever touching the backend LLM or the adapter path.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ from litellm.types.llms.anthropic_messages.anthropic_response import (
|
|||
AnthropicMessagesResponse,
|
||||
)
|
||||
from litellm.types.llms.anthropic import ANTHROPIC_ADVISOR_TOOL_TYPE
|
||||
from litellm.utils import (
|
||||
resolve_proxy_model_alias_to_litellm_model,
|
||||
supports_native_advisor_tool,
|
||||
)
|
||||
|
||||
ADVISOR_MAX_USES: int = _c.ADVISOR_MAX_USES
|
||||
ADVISOR_TOOL_DESCRIPTION: str = _c.ADVISOR_TOOL_DESCRIPTION
|
||||
|
|
@ -203,43 +207,13 @@ def _resolve_default_advisor_model() -> str:
|
|||
return params.get("default_advisor_model", "") or ""
|
||||
|
||||
|
||||
def _is_anthropic_opus_46_model(model: str) -> bool:
|
||||
"""Return True for Anthropic Claude Opus 4.6 model identifiers."""
|
||||
normalized = model.lower().replace("_", "-")
|
||||
return "anthropic/" in normalized and "claude-opus-4-6" in normalized
|
||||
|
||||
|
||||
def _resolve_proxy_model_alias_to_litellm_model(model: str) -> str:
|
||||
"""
|
||||
Resolve a proxy ``model_name`` alias to its configured ``litellm_params.model``.
|
||||
|
||||
Example: ``claude_opus`` -> ``anthropic/claude-opus-4-6``.
|
||||
"""
|
||||
try:
|
||||
from litellm.proxy.proxy_server import llm_router
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
model_list = getattr(llm_router, "model_list", None) or []
|
||||
for deployment in model_list:
|
||||
if not isinstance(deployment, dict):
|
||||
continue
|
||||
if deployment.get("model_name") != model:
|
||||
continue
|
||||
litellm_params = deployment.get("litellm_params") or {}
|
||||
configured_model = litellm_params.get("model")
|
||||
if isinstance(configured_model, str):
|
||||
return configured_model
|
||||
return ""
|
||||
|
||||
|
||||
def _should_use_native_anthropic_advisor(
|
||||
tools: List[Dict], custom_llm_provider: Optional[str]
|
||||
) -> bool:
|
||||
"""
|
||||
Use Anthropic's native advisor path only when:
|
||||
- executor provider is Anthropic, and
|
||||
- advisor model resolves to Anthropic Claude Opus 4.6.
|
||||
- advisor model supports the native advisor capability.
|
||||
"""
|
||||
if custom_llm_provider != "anthropic":
|
||||
return False
|
||||
|
|
@ -255,12 +229,12 @@ def _should_use_native_anthropic_advisor(
|
|||
if not advisor_model:
|
||||
return False
|
||||
|
||||
if _is_anthropic_opus_46_model(advisor_model):
|
||||
return True
|
||||
|
||||
# Proxy requests commonly pass advisor model as a model_name alias.
|
||||
resolved_proxy_model = _resolve_proxy_model_alias_to_litellm_model(advisor_model)
|
||||
if _is_anthropic_opus_46_model(resolved_proxy_model):
|
||||
resolved_proxy_model = resolve_proxy_model_alias_to_litellm_model(advisor_model)
|
||||
model_to_check = resolved_proxy_model or advisor_model
|
||||
if supports_native_advisor_tool(
|
||||
model=model_to_check, custom_llm_provider="anthropic"
|
||||
):
|
||||
return True
|
||||
|
||||
try:
|
||||
|
|
@ -269,8 +243,8 @@ def _should_use_native_anthropic_advisor(
|
|||
resolved_model, advisor_provider, _, _ = litellm.get_llm_provider(
|
||||
model=advisor_model
|
||||
)
|
||||
return advisor_provider == "anthropic" and _is_anthropic_opus_46_model(
|
||||
resolved_model
|
||||
return advisor_provider == "anthropic" and supports_native_advisor_tool(
|
||||
model=resolved_model, custom_llm_provider=advisor_provider
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from litellm.types.llms.anthropic_messages.anthropic_response import (
|
|||
)
|
||||
from litellm.types.llms.anthropic_tool_search import get_tool_search_beta_header
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.utils import resolve_proxy_model_alias_to_litellm_model
|
||||
|
||||
from ...common_utils import (
|
||||
AnthropicError,
|
||||
|
|
@ -28,26 +29,6 @@ from ...common_utils import (
|
|||
DEFAULT_ANTHROPIC_API_VERSION = "2023-06-01"
|
||||
|
||||
|
||||
def _resolve_proxy_model_alias_to_litellm_model(model: str) -> str:
|
||||
"""Resolve proxy model_name alias to configured litellm model string."""
|
||||
try:
|
||||
from litellm.proxy.proxy_server import llm_router
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
model_list = getattr(llm_router, "model_list", None) or []
|
||||
for deployment in model_list:
|
||||
if not isinstance(deployment, dict):
|
||||
continue
|
||||
if deployment.get("model_name") != model:
|
||||
continue
|
||||
litellm_params = deployment.get("litellm_params") or {}
|
||||
configured_model = litellm_params.get("model")
|
||||
if isinstance(configured_model, str):
|
||||
return configured_model
|
||||
return ""
|
||||
|
||||
|
||||
def _normalize_anthropic_advisor_tool_models(tools: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
Normalize advisor tool model names for Anthropic native /v1/messages calls.
|
||||
|
|
@ -68,7 +49,7 @@ def _normalize_anthropic_advisor_tool_models(tools: List[Dict]) -> List[Dict]:
|
|||
updated_tool = dict(tool)
|
||||
advisor_model = updated_tool.get("model")
|
||||
if isinstance(advisor_model, str) and advisor_model.strip():
|
||||
resolved = _resolve_proxy_model_alias_to_litellm_model(advisor_model.strip())
|
||||
resolved = resolve_proxy_model_alias_to_litellm_model(advisor_model.strip())
|
||||
canonical_model = resolved or advisor_model.strip()
|
||||
if canonical_model.startswith("anthropic/"):
|
||||
canonical_model = canonical_model.split("/", 1)[1]
|
||||
|
|
|
|||
|
|
@ -726,6 +726,7 @@
|
|||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_native_advisor_tool": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -749,6 +750,7 @@
|
|||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_native_advisor_tool": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ class ProviderSpecificModelInfo(TypedDict, total=False):
|
|||
supports_parallel_function_calling: Optional[bool]
|
||||
supports_web_search: Optional[bool]
|
||||
supports_reasoning: Optional[bool]
|
||||
supports_native_advisor_tool: Optional[bool]
|
||||
supports_url_context: Optional[bool]
|
||||
supports_none_reasoning_effort: Optional[bool]
|
||||
supports_xhigh_reasoning_effort: Optional[bool]
|
||||
|
|
|
|||
|
|
@ -2734,6 +2734,43 @@ def supports_reasoning(model: str, custom_llm_provider: Optional[str] = None) ->
|
|||
)
|
||||
|
||||
|
||||
def resolve_proxy_model_alias_to_litellm_model(model: str) -> str:
|
||||
"""Resolve a proxy ``model_name`` alias to configured ``litellm_params.model``."""
|
||||
try:
|
||||
from litellm.proxy.proxy_server import llm_router
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
try:
|
||||
model_list = llm_router.get_model_list(model_name=model) or []
|
||||
except Exception:
|
||||
model_list = getattr(llm_router, "model_list", None) or []
|
||||
|
||||
for deployment in model_list:
|
||||
if not isinstance(deployment, dict):
|
||||
continue
|
||||
litellm_params = deployment.get("litellm_params") or {}
|
||||
configured_model = litellm_params.get("model")
|
||||
if isinstance(configured_model, str):
|
||||
return configured_model
|
||||
return ""
|
||||
|
||||
|
||||
def supports_native_advisor_tool(
|
||||
model: str, custom_llm_provider: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Check if the given model supports Anthropic's native advisor tool.
|
||||
"""
|
||||
resolved_model = resolve_proxy_model_alias_to_litellm_model(model)
|
||||
model_to_check = resolved_model or model
|
||||
return _supports_factory(
|
||||
model=model_to_check,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
key="supports_native_advisor_tool",
|
||||
)
|
||||
|
||||
|
||||
def supports_native_structured_output(
|
||||
model: str, custom_llm_provider: Optional[str] = None
|
||||
) -> bool:
|
||||
|
|
|
|||
|
|
@ -8920,6 +8920,7 @@
|
|||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_native_advisor_tool": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -8951,6 +8952,7 @@
|
|||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_native_advisor_tool": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
|
|||
|
|
@ -59,11 +59,15 @@ async def test_pre_request_hook_anthropic_converts_standard_tool_to_native():
|
|||
"litellm_params": {"custom_llm_provider": "anthropic"},
|
||||
}
|
||||
|
||||
result = await logger.async_pre_request_hook(
|
||||
model="claude-sonnet-4-6",
|
||||
messages=[{"role": "user", "content": "Help"}],
|
||||
kwargs=kwargs,
|
||||
)
|
||||
with patch(
|
||||
"litellm.integrations.advisor_interception.handler.supports_native_advisor_tool",
|
||||
return_value=True,
|
||||
):
|
||||
result = await logger.async_pre_request_hook(
|
||||
model="claude-sonnet-4-6",
|
||||
messages=[{"role": "user", "content": "Help"}],
|
||||
kwargs=kwargs,
|
||||
)
|
||||
assert result is not None
|
||||
tool = result["tools"][0]
|
||||
assert tool["type"] == "advisor_20260301"
|
||||
|
|
@ -551,6 +555,33 @@ def test_default_advisor_model_is_none_by_default():
|
|||
assert logger.default_advisor_model is None
|
||||
|
||||
|
||||
def test_is_native_anthropic_advisor_model_delegates_to_model_capability(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"litellm.integrations.advisor_interception.handler.resolve_proxy_model_alias_to_litellm_model",
|
||||
lambda model: "anthropic/new-native-advisor-model"
|
||||
if model == "proxy_alias"
|
||||
else "",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"litellm.integrations.advisor_interception.handler.supports_native_advisor_tool",
|
||||
lambda model, custom_llm_provider=None: (
|
||||
custom_llm_provider == "anthropic"
|
||||
and model == "anthropic/new-native-advisor-model"
|
||||
),
|
||||
)
|
||||
|
||||
assert (
|
||||
AdvisorInterceptionLogger._is_native_anthropic_advisor_model("proxy_alias")
|
||||
is True
|
||||
)
|
||||
assert (
|
||||
AdvisorInterceptionLogger._is_native_anthropic_advisor_model(
|
||||
"anthropic/claude-opus-4-6"
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
def test_convert_tools_raises_when_no_advisor_model():
|
||||
logger = AdvisorInterceptionLogger()
|
||||
kwargs = {
|
||||
|
|
@ -604,6 +635,28 @@ async def test_run_agentic_loop_raises_when_no_advisor_model():
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_log_failure_event_cleans_up_call_tracking_sets():
|
||||
logger = AdvisorInterceptionLogger(enabled_providers=["openai"])
|
||||
logger._advisor_config_by_call_id["failed-call"] = {
|
||||
"advisor_model": "claude-opus-4-6",
|
||||
"max_uses": 2,
|
||||
}
|
||||
logger._converted_stream_call_ids.add("failed-call")
|
||||
logger._skip_post_hook_call_ids.add("failed-call")
|
||||
|
||||
await logger.async_log_failure_event(
|
||||
kwargs={"litellm_call_id": "failed-call"},
|
||||
response_obj=Exception("boom"),
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
)
|
||||
|
||||
assert "failed-call" not in logger._advisor_config_by_call_id
|
||||
assert "failed-call" not in logger._converted_stream_call_ids
|
||||
assert "failed-call" not in logger._skip_post_hook_call_ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agentic_loop_uses_router_when_available(monkeypatch):
|
||||
logger = AdvisorInterceptionLogger(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from typing import Dict
|
|||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import litellm
|
||||
|
||||
ADVISOR_TOOL = {
|
||||
"type": "advisor_20260301",
|
||||
|
|
@ -34,7 +35,9 @@ def _text_resp(text: str, model: str = "gpt-4o-mini") -> Dict:
|
|||
}
|
||||
|
||||
|
||||
def _advisor_call_resp(question: str = "How do I approach this?", tool_id: str = "tid_01") -> Dict:
|
||||
def _advisor_call_resp(
|
||||
question: str = "How do I approach this?", tool_id: str = "tid_01"
|
||||
) -> Dict:
|
||||
return {
|
||||
"id": "msg_int_test",
|
||||
"type": "message",
|
||||
|
|
@ -44,7 +47,7 @@ def _advisor_call_resp(question: str = "How do I approach this?", tool_id: str =
|
|||
{
|
||||
"type": "tool_use",
|
||||
"id": tool_id,
|
||||
"name": "advisor",
|
||||
"name": "consult_advisor",
|
||||
"input": {"question": question},
|
||||
}
|
||||
],
|
||||
|
|
@ -93,8 +96,9 @@ async def test_full_dispatch_interceptor_fires_and_loop_completes():
|
|||
custom_llm_provider="openai",
|
||||
)
|
||||
|
||||
# 3 internal calls: executor → advisor → executor-final
|
||||
assert call_count == 3
|
||||
# 2 calls to _call_messages_handler: executor -> executor-final.
|
||||
# Advisor subcall runs via _call_advisor_with_router (acompletion path).
|
||||
assert call_count == 2
|
||||
|
||||
assert isinstance(result, dict)
|
||||
content = result.get("content", [])
|
||||
|
|
@ -183,3 +187,66 @@ async def test_anthropic_provider_bypasses_interceptor():
|
|||
content = result.get("content", []) if isinstance(result, dict) else []
|
||||
text_blocks = [b for b in content if b.get("type") == "text"]
|
||||
assert any("Native anthropic" in b.get("text", "") for b in text_blocks)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_messages_interceptor_path_still_runs_non_advisor_pre_request_hooks():
|
||||
"""
|
||||
Pre-request hooks should still run for intercepted /messages requests, while
|
||||
advisor tool conversion must be skipped so interceptors can still see the
|
||||
native advisor_20260301 tool.
|
||||
"""
|
||||
from litellm.integrations.advisor_interception.handler import (
|
||||
AdvisorInterceptionLogger,
|
||||
)
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages.handler import (
|
||||
anthropic_messages,
|
||||
)
|
||||
|
||||
class _NonAdvisorHookLogger(CustomLogger):
|
||||
async def async_pre_request_hook(self, model, messages, kwargs):
|
||||
kwargs["metadata"] = {"from_non_advisor_hook": True}
|
||||
return kwargs
|
||||
|
||||
class _CaptureInterceptor:
|
||||
def __init__(self):
|
||||
self.seen_kwargs = {}
|
||||
self.seen_tools = None
|
||||
|
||||
def can_handle(self, tools, custom_llm_provider):
|
||||
return True
|
||||
|
||||
async def handle(self, **kwargs):
|
||||
self.seen_kwargs = kwargs
|
||||
self.seen_tools = kwargs.get("tools")
|
||||
return _text_resp("interceptor handled request")
|
||||
|
||||
capture_interceptor = _CaptureInterceptor()
|
||||
original_callbacks = litellm.callbacks
|
||||
try:
|
||||
litellm.callbacks = [
|
||||
AdvisorInterceptionLogger(default_advisor_model="claude-opus-4-6"),
|
||||
_NonAdvisorHookLogger(),
|
||||
]
|
||||
with patch(
|
||||
"litellm.llms.anthropic.experimental_pass_through.messages.handler.get_messages_interceptors",
|
||||
return_value=[capture_interceptor],
|
||||
):
|
||||
result = await anthropic_messages(
|
||||
model="openai/gpt-4o-mini",
|
||||
messages=MESSAGES,
|
||||
tools=[ADVISOR_TOOL],
|
||||
stream=False,
|
||||
max_tokens=256,
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
finally:
|
||||
litellm.callbacks = original_callbacks
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert capture_interceptor.seen_kwargs.get("metadata") == {
|
||||
"from_non_advisor_hook": True
|
||||
}
|
||||
assert isinstance(capture_interceptor.seen_tools, list)
|
||||
assert capture_interceptor.seen_tools[0].get("type") == "advisor_20260301"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue