Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions litellm/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
from litellm.litellm_core_utils.litellm_logging import Logging

from litellm.constants import (
DEFAULT_CHAT_COMPLETION_PARAM_VALUES,
DEFAULT_MOCK_RESPONSE_COMPLETION_TOKEN_COUNT,
DEFAULT_MOCK_RESPONSE_PROMPT_TOKEN_COUNT,
)
Expand Down Expand Up @@ -1487,6 +1488,13 @@ def completion( # type: ignore # noqa: PLR0915
"service_tier": service_tier,
"allowed_openai_params": kwargs.get("allowed_openai_params"),
}
for k, v in kwargs.items():
if (
k in DEFAULT_CHAT_COMPLETION_PARAM_VALUES
and k not in optional_param_args
and v is not None
):
optional_param_args[k] = v
Comment on lines +1491 to +1497

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safety net is not applied in the fallbacks path of acompletion

When fallbacks is provided, acompletion() routes through async_completion_with_fallbacks():

response = await async_completion_with_fallbacks(
    **completion_kwargs, kwargs={"fallbacks": fallbacks, **kwargs}
)

completion_kwargs is the static dict built at the top of acompletion(), which does not include store (or any other param captured only by the safety net in completion()). store survives here only because it is also forwarded in the outer **kwargs dict — but that reliance on the shape of async_completion_with_fallbacks's internal unpacking is fragile. The same "dead zone" that originally afflicted store could reappear for this code path if async_completion_with_fallbacks stops propagating the inner kwargs faithfully.

The most robust fix would be to add store (and other params that belong in DEFAULT_CHAT_COMPLETION_PARAM_VALUES but are missing from the function signature) as explicit named parameters in both completion() and acompletion() and include them in completion_kwargs. This makes the data flow explicit rather than relying on the safety net.

optional_params = get_optional_params(
**optional_param_args, **non_default_params
)
Expand Down
188 changes: 188 additions & 0 deletions tests/test_litellm/llms/openai/chat/test_store_param.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
"""
Tests for the `store` parameter being correctly forwarded to OpenAI.

Related issue: https://github.com/BerriAI/litellm/issues/23087

The `store` parameter was listed in OPENAI_CHAT_COMPLETION_PARAMS and
DEFAULT_CHAT_COMPLETION_PARAM_VALUES but was silently dropped because
get_non_default_completion_params() excluded it (as a "known" param)
while optional_param_args didn't include it (not a named param of
completion()). The fix adds a safety net in completion() that forwards
any kwargs present in DEFAULT_CHAT_COMPLETION_PARAM_VALUES that aren't
already in optional_param_args.
"""

import os
import sys

sys.path.insert(0, os.path.abspath("../../../../.."))

from litellm.constants import DEFAULT_CHAT_COMPLETION_PARAM_VALUES
from litellm.utils import get_non_default_completion_params, get_optional_params


class TestStoreParamForwarding:
"""Tests that `store` flows through the parameter processing pipeline."""

def test_store_true_forwarded_for_openai(self):
"""should forward store=True for OpenAI models via kwargs"""
result = get_optional_params(
model="gpt-5.1",
custom_llm_provider="openai",
store=True,
)
assert result.get("store") is True

def test_store_false_forwarded_for_openai(self):
"""should forward store=False for OpenAI models"""
result = get_optional_params(
model="gpt-4o",
custom_llm_provider="openai",
store=False,
)
assert result.get("store") is False

def test_store_none_not_forwarded(self):
"""should not include store when it is None (default)"""
result = get_optional_params(
model="gpt-4o",
custom_llm_provider="openai",
)
assert "store" not in result

def test_store_with_gpt5_models(self):
"""should forward store=True for GPT-5 family models"""
for model in ["gpt-5.1", "gpt-5.2"]:
result = get_optional_params(
model=model,
custom_llm_provider="openai",
store=True,
)
assert result.get("store") is True, f"store not forwarded for {model}"

def test_store_in_supported_params(self):
"""should list store as a supported OpenAI param"""
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig

config = OpenAIGPTConfig()
for model in ["gpt-4o", "gpt-5.1", "gpt-5.2"]:
supported = config.get_supported_openai_params(model)
assert "store" in supported, f"store not in supported params for {model}"

def test_store_in_transform_request(self):
"""should include store in the final transformed request body"""
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig

config = OpenAIGPTConfig()
messages = [{"role": "user", "content": "Hello"}]
optional_params = {"store": True}
result = config.transform_request(
model="gpt-5.1",
messages=messages,
optional_params=optional_params,
litellm_params={},
headers={},
)
assert result.get("store") is True

def test_store_true_with_metadata(self):
"""should forward both store and metadata when both are set"""
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig

config = OpenAIGPTConfig()
messages = [{"role": "user", "content": "Hello"}]
optional_params = {"store": True, "metadata": {"key": "value"}}
result = config.transform_request(
model="gpt-5.1",
messages=messages,
optional_params=optional_params,
litellm_params={},
headers={},
)
assert result.get("store") is True
assert result.get("metadata") == {"key": "value"}


class TestDefaultParamValuesSafetyNet:
"""Tests that any param in DEFAULT_CHAT_COMPLETION_PARAM_VALUES flows
through completion() even without being a named parameter."""

def test_known_openai_param_excluded_from_non_default(self):
"""should confirm get_non_default_completion_params excludes known OpenAI params"""
kwargs = {"store": True, "temperature": 0.5}
non_default = get_non_default_completion_params(kwargs=kwargs)
assert "store" not in non_default
assert "temperature" not in non_default

def test_unknown_param_included_in_non_default(self):
"""should pass through unknown provider-specific params"""
kwargs = {"my_custom_provider_param": "foo"}
non_default = get_non_default_completion_params(kwargs=kwargs)
assert non_default.get("my_custom_provider_param") == "foo"

def test_safety_net_forwards_recognized_kwargs(self):
"""should forward kwargs in DEFAULT_CHAT_COMPLETION_PARAM_VALUES
that are not already in optional_param_args"""
optional_param_args = {
"model": "gpt-5.1",
"custom_llm_provider": "openai",
"temperature": 0.7,
}
kwargs = {"store": True, "metadata": {"key": "value"}}
for k, v in kwargs.items():
if (
k in DEFAULT_CHAT_COMPLETION_PARAM_VALUES
and k not in optional_param_args
and v is not None
):
optional_param_args[k] = v

assert optional_param_args["store"] is True
assert optional_param_args["metadata"] == {"key": "value"}
assert optional_param_args["temperature"] == 0.7

def test_safety_net_does_not_override_existing(self):
"""should not override a param that's already in optional_param_args"""
optional_param_args = {
"model": "gpt-5.1",
"custom_llm_provider": "openai",
"temperature": 0.7,
}
kwargs = {"temperature": 0.9}
for k, v in kwargs.items():
if (
k in DEFAULT_CHAT_COMPLETION_PARAM_VALUES
and k not in optional_param_args
and v is not None
):
optional_param_args[k] = v

assert optional_param_args["temperature"] == 0.7

def test_safety_net_skips_none_values(self):
"""should not forward params with None value (the default)"""
optional_param_args = {"model": "gpt-5.1", "custom_llm_provider": "openai"}
kwargs = {"store": None}
for k, v in kwargs.items():
if (
k in DEFAULT_CHAT_COMPLETION_PARAM_VALUES
and k not in optional_param_args
and v is not None
):
optional_param_args[k] = v

assert "store" not in optional_param_args

def test_safety_net_forwards_falsy_non_none(self):
"""should forward store=False (falsy but not None)"""
optional_param_args = {"model": "gpt-5.1", "custom_llm_provider": "openai"}
kwargs = {"store": False}
for k, v in kwargs.items():
if (
k in DEFAULT_CHAT_COMPLETION_PARAM_VALUES
and k not in optional_param_args
and v is not None
):
optional_param_args[k] = v

assert optional_param_args.get("store") is False
Loading