From 42ca54b911e284cdfaca336c6481e7cd054689e0 Mon Sep 17 00:00:00 2001 From: Devauntae Date: Mon, 5 Oct 2026 16:19:25 -0400 Subject: [PATCH] fix(ollama): forward frequency/presence penalties under their own names ollama_chat mapped OpenAI's frequency_penalty to Ollama's repeat_penalty. The two are different settings: OpenAI's penalties are additive (0 = off), while repeat_penalty is multiplicative (1.0 = off), so frequency_penalty=0 arrived as repeat_penalty=0. presence_penalty was rejected as unsupported. Forward frequency_penalty and presence_penalty unchanged, as the completion path already does for frequency_penalty, and add presence_penalty there too. repeat_penalty can still be passed directly. --- litellm/llms/ollama/chat/transformation.py | 8 +++- .../llms/ollama/completion/transformation.py | 3 ++ .../ollama/test_ollama_chat_transformation.py | 40 +++++++++++++++++++ .../test_ollama_completion_transformation.py | 12 ++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/litellm/llms/ollama/chat/transformation.py b/litellm/llms/ollama/chat/transformation.py index 59020b058a2..af0abffe300 100644 --- a/litellm/llms/ollama/chat/transformation.py +++ b/litellm/llms/ollama/chat/transformation.py @@ -140,6 +140,7 @@ class OllamaChatConfig(BaseConfig): "temperature", "seed", "frequency_penalty", + "presence_penalty", "stop", "tools", "tool_choice", @@ -172,8 +173,13 @@ class OllamaChatConfig(BaseConfig): optional_params["seed"] = value if param == "top_p": optional_params["top_p"] = value + # Ollama exposes OpenAI's additive penalties under the same names. + # `repeat_penalty` is a different, multiplicative setting (1.0 = off), + # so forwarding an OpenAI value of 0 to it would invert the penalty. if param == "frequency_penalty": - optional_params["repeat_penalty"] = value + optional_params["frequency_penalty"] = value + if param == "presence_penalty": + optional_params["presence_penalty"] = value if param == "stop": optional_params["stop"] = value if param == "response_format" and isinstance(value, dict) and value.get("type") == "json_object": diff --git a/litellm/llms/ollama/completion/transformation.py b/litellm/llms/ollama/completion/transformation.py index 6dbcb050e11..76eeb13cc37 100644 --- a/litellm/llms/ollama/completion/transformation.py +++ b/litellm/llms/ollama/completion/transformation.py @@ -221,6 +221,7 @@ class OllamaConfig(BaseConfig): "temperature", "seed", "frequency_penalty", + "presence_penalty", "stop", "response_format", "max_completion_tokens", @@ -247,6 +248,8 @@ class OllamaConfig(BaseConfig): optional_params["top_p"] = value elif param == "frequency_penalty": optional_params["frequency_penalty"] = value + elif param == "presence_penalty": + optional_params["presence_penalty"] = value elif param == "stop": optional_params["stop"] = value elif param == "reasoning_effort" and value is not None: diff --git a/tests/unit/llms/ollama/test_ollama_chat_transformation.py b/tests/unit/llms/ollama/test_ollama_chat_transformation.py index 98ca5aee407..71deb26cc6d 100644 --- a/tests/unit/llms/ollama/test_ollama_chat_transformation.py +++ b/tests/unit/llms/ollama/test_ollama_chat_transformation.py @@ -1309,3 +1309,43 @@ def test_reasoning_effort_with_a_matching_think_value_does_not_warn( assert result["think"] is think assert _warnings_naming(caplog, "reasoning_effort") == () + + +class TestOllamaChatPenaltyParams: + """OpenAI's penalties are additive (0 = off); Ollama's `repeat_penalty` is multiplicative (1.0 = off).""" + + def test_frequency_penalty_is_forwarded_under_its_own_name(self): + optional_params = get_optional_params( + model="ollama_chat/test-model", + frequency_penalty=0, + custom_llm_provider="ollama_chat", + ) + + assert optional_params["frequency_penalty"] == 0 + assert "repeat_penalty" not in optional_params + + def test_presence_penalty_is_forwarded(self): + optional_params = get_optional_params( + model="ollama_chat/test-model", + presence_penalty=0.5, + custom_llm_provider="ollama_chat", + ) + + assert optional_params["presence_penalty"] == 0.5 + + def test_presence_penalty_is_a_supported_param(self): + supported = OllamaChatConfig().get_supported_openai_params("test-model") + + assert "presence_penalty" in supported + assert "frequency_penalty" in supported + + def test_explicit_repeat_penalty_is_passed_through(self): + optional_params = get_optional_params( + model="ollama_chat/test-model", + frequency_penalty=0.2, + repeat_penalty=1.3, + custom_llm_provider="ollama_chat", + ) + + assert optional_params["repeat_penalty"] == 1.3 + assert optional_params["frequency_penalty"] == 0.2 diff --git a/tests/unit/llms/ollama/test_ollama_completion_transformation.py b/tests/unit/llms/ollama/test_ollama_completion_transformation.py index 26addf1b24c..f5331212510 100644 --- a/tests/unit/llms/ollama/test_ollama_completion_transformation.py +++ b/tests/unit/llms/ollama/test_ollama_completion_transformation.py @@ -1100,3 +1100,15 @@ def test_reasoning_effort_ollama_honors_does_not_warn( assert result["think"] == think assert _reasoning_effort_warnings(caplog) == () + + +def test_map_openai_params_forwards_presence_penalty() -> None: + config = OllamaConfig() + + assert "presence_penalty" in config.get_supported_openai_params("test-model") + assert config.map_openai_params( + non_default_params={"presence_penalty": 0.5}, + optional_params={}, + model="test-model", + drop_params=False, + ) == {"presence_penalty": 0.5}