diff --git a/mem0/configs/llms/anthropic.py b/mem0/configs/llms/anthropic.py index 45552eeba..7c04ebf2b 100644 --- a/mem0/configs/llms/anthropic.py +++ b/mem0/configs/llms/anthropic.py @@ -23,6 +23,7 @@ class AnthropicConfig(BaseLlmConfig): http_client_proxies: Optional[dict] = None, # Anthropic-specific parameters anthropic_base_url: Optional[str] = None, + enable_sampling_parameters: Optional[bool] = None, ): """ Initialize Anthropic configuration. @@ -38,6 +39,7 @@ class AnthropicConfig(BaseLlmConfig): vision_details: Vision detail level, defaults to "auto" http_client_proxies: HTTP client proxy settings, defaults to None anthropic_base_url: Anthropic API base URL, defaults to None + enable_sampling_parameters: Enable sampling parameters, defaults to None """ # Initialize base parameters super().__init__( @@ -54,3 +56,4 @@ class AnthropicConfig(BaseLlmConfig): # Anthropic-specific parameters self.anthropic_base_url = anthropic_base_url + self.enable_sampling_parameters = enable_sampling_parameters diff --git a/mem0/llms/anthropic.py b/mem0/llms/anthropic.py index 179f1b4ff..3b6d7723a 100644 --- a/mem0/llms/anthropic.py +++ b/mem0/llms/anthropic.py @@ -44,6 +44,30 @@ class AnthropicLLM(LLMBase): client_kwargs["base_url"] = base_url self.client = anthropic.Anthropic(**client_kwargs) + def _enable_sampling_parameters(self): + """Return whether the configured model supports sampling parameters.""" + explicit = getattr(self.config, "enable_sampling_parameters", None) + if explicit is not None: + return explicit + + model_name = self.config.model.lower() + model_name_parts = model_name.rsplit("[", 1)[0].split("-") + _, family, major, minor, *_ = (*model_name_parts, "", "", "", "") + + # Use model_name if only provided a family name, e.g., opus + family = family or model_name + major = int(major) if major.isdigit() else None + minor = int(minor) if minor.isdigit() else None + + if family == "haiku": + return True + if family == "sonnet" and major is not None: + return major < 5 + if family == "opus" and major is not None and minor is not None: + return (major, minor) < (4, 7) + + return False + def _get_common_params(self, **kwargs) -> Dict: """Get common parameters, avoiding sending both temperature and top_p together. @@ -58,13 +82,14 @@ class AnthropicLLM(LLMBase): has_temperature = self.config.temperature is not None has_top_p = self.config.top_p is not None - if has_temperature and has_top_p: - # Anthropic forbids both; prefer temperature - params["temperature"] = self.config.temperature - elif has_temperature: - params["temperature"] = self.config.temperature - elif has_top_p: - params["top_p"] = self.config.top_p + if self._enable_sampling_parameters(): + if has_temperature and has_top_p: + # Anthropic forbids both; prefer temperature + params["temperature"] = self.config.temperature + elif has_temperature: + params["temperature"] = self.config.temperature + elif has_top_p: + params["top_p"] = self.config.top_p params.update(kwargs) return params @@ -122,8 +147,6 @@ class AnthropicLLM(LLMBase): if block.type == "text": result["content"] = block.text elif block.type == "tool_use": - result["tool_calls"].append( - {"name": block.name, "arguments": block.input} - ) + result["tool_calls"].append({"name": block.name, "arguments": block.input}) return result return response.content[0].text diff --git a/tests/llms/test_anthropic.py b/tests/llms/test_anthropic.py index a0d103c2b..fd20326c4 100644 --- a/tests/llms/test_anthropic.py +++ b/tests/llms/test_anthropic.py @@ -19,14 +19,14 @@ def mock_anthropic_client(): def test_default_config_omits_top_p(mock_anthropic_client): """Default AnthropicConfig should not set top_p to avoid conflict with temperature.""" - config = AnthropicConfig(model="claude-3-5-sonnet-20240620", api_key="test-key") + config = AnthropicConfig(model="claude-opus-4-6", api_key="test-key") assert config.top_p is None assert config.temperature == 0.1 def test_generate_response_does_not_send_top_p_by_default(mock_anthropic_client): """Anthropic API rejects temperature and top_p together; top_p must be omitted by default.""" - config = AnthropicConfig(model="claude-3-5-sonnet-20240620", api_key="test-key") + config = AnthropicConfig(model="claude-opus-4-6", api_key="test-key") llm = AnthropicLLM(config) mock_response = Mock() @@ -47,7 +47,7 @@ def test_generate_response_does_not_send_top_p_by_default(mock_anthropic_client) def test_generate_response_sends_top_p_alone_when_no_temperature(mock_anthropic_client): """When user sets only top_p (no temperature), top_p should be sent.""" - config = AnthropicConfig(model="claude-3-5-sonnet-20240620", api_key="test-key", top_p=0.9, temperature=None) + config = AnthropicConfig(model="claude-opus-4-6", api_key="test-key", top_p=0.9, temperature=None) llm = AnthropicLLM(config) mock_response = Mock() @@ -68,7 +68,7 @@ def test_generate_response_sends_top_p_alone_when_no_temperature(mock_anthropic_ def test_both_set_prefers_temperature_over_top_p(mock_anthropic_client): """When both temperature and top_p are set, temperature wins and top_p is dropped.""" - config = AnthropicConfig(model="claude-3-5-sonnet-20240620", api_key="test-key", top_p=0.9, temperature=0.5) + config = AnthropicConfig(model="claude-opus-4-6", api_key="test-key", top_p=0.9, temperature=0.5) llm = AnthropicLLM(config) mock_response = Mock() @@ -87,7 +87,7 @@ def test_configured_base_url_is_passed_to_client(): """A configured anthropic_base_url must reach the Anthropic client constructor.""" with patch("mem0.llms.anthropic.anthropic") as mock_anthropic: config = AnthropicConfig( - model="claude-3-5-sonnet-20240620", + model="claude-opus-4-6", api_key="test-key", anthropic_base_url="https://proxy.example.com", ) @@ -100,7 +100,7 @@ def test_base_url_falls_back_to_env(monkeypatch): """When no base_url is configured, ANTHROPIC_BASE_URL is used.""" monkeypatch.setenv("ANTHROPIC_BASE_URL", "https://env.example.com") with patch("mem0.llms.anthropic.anthropic") as mock_anthropic: - config = AnthropicConfig(model="claude-3-5-sonnet-20240620", api_key="test-key") + config = AnthropicConfig(model="claude-opus-4-6", api_key="test-key") AnthropicLLM(config) assert mock_anthropic.Anthropic.call_args[1]["base_url"] == "https://env.example.com" @@ -110,7 +110,7 @@ def test_base_url_omitted_when_unset(monkeypatch): """With no base_url anywhere, base_url must not be forced to None on the client.""" monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False) with patch("mem0.llms.anthropic.anthropic") as mock_anthropic: - config = AnthropicConfig(model="claude-3-5-sonnet-20240620", api_key="test-key") + config = AnthropicConfig(model="claude-opus-4-6", api_key="test-key") AnthropicLLM(config) assert "base_url" not in mock_anthropic.Anthropic.call_args[1] @@ -118,7 +118,7 @@ def test_base_url_omitted_when_unset(monkeypatch): def test_base_config_conversion_does_not_send_both(mock_anthropic_client): """BaseLlmConfig defaults both temperature=0.1 and top_p=0.1; Anthropic must not send both.""" - base_config = BaseLlmConfig(model="claude-3-5-sonnet-20240620", api_key="test-key") + base_config = BaseLlmConfig(model="claude-opus-4-6", api_key="test-key") llm = AnthropicLLM(base_config) mock_response = Mock() @@ -131,3 +131,57 @@ def test_base_config_conversion_does_not_send_both(mock_anthropic_client): call_kwargs = mock_anthropic_client.messages.create.call_args[1] assert "temperature" in call_kwargs assert "top_p" not in call_kwargs + + +@pytest.mark.parametrize( + ("model", "expected"), + [ + ("opus", False), + ("claude-opus-4-6", True), + ("claude-opus-4-7", False), + ("claude-opus-4-8", False), + ("sonnet", False), + ("claude-sonnet-4-6", True), + ("claude-sonnet-5", False), + ("haiku", True), + ("claude-haiku-4-5", True), + ("claude-haiku-4-5-20251001", True), + ("claude-fable-5", False), + ("claude-mythos-5", False), + ("claude-mythos-preview", False), + ], +) +def test_enable_sampling_parameters_for_current_models(mock_anthropic_client, model, expected): + config = AnthropicConfig(model=model, api_key="test-key", top_p=0.9) + llm = AnthropicLLM(config) + + mock_response = Mock() + mock_response.content = [Mock(text="Hello!")] + mock_anthropic_client.messages.create.return_value = mock_response + + messages = [{"role": "user", "content": "Hi"}] + llm.generate_response(messages) + + call_kwargs = mock_anthropic_client.messages.create.call_args[1] + assert ("temperature" in call_kwargs) is expected + + +def test_enable_sampling_parameters_respects_explicit_override(mock_anthropic_client): + config = AnthropicConfig( + model="claude-opus-4-6", + api_key="test-key", + top_p=0.9, + enable_sampling_parameters=False, + ) + llm = AnthropicLLM(config) + + mock_response = Mock() + mock_response.content = [Mock(text="Hello!")] + mock_anthropic_client.messages.create.return_value = mock_response + + messages = [{"role": "user", "content": "Hi"}] + llm.generate_response(messages) + + call_kwargs = mock_anthropic_client.messages.create.call_args[1] + assert "temperature" not in call_kwargs + assert "top_p" not in call_kwargs