diff --git a/litellm/constants.py b/litellm/constants.py index 6b984c2673c..de00c682d04 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -792,6 +792,9 @@ OPENAI_TRANSCRIPTION_PARAMS: Final = [ OPENAI_EMBEDDING_PARAMS: Final = ["dimensions", "encoding_format", "user"] +# excludes extra_query: it has no forwarding path outside extra_body yet +OPENAI_SDK_TRANSPORT_PARAMS: Final = frozenset({"extra_headers", "timeout"}) + DEFAULT_EMBEDDING_PARAM_VALUES: Final = { **{k: None for k in OPENAI_EMBEDDING_PARAMS}, "model": None, diff --git a/litellm/utils.py b/litellm/utils.py index 1a77655a5a4..ed353092c51 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -78,6 +78,7 @@ from litellm.constants import ( MINIMUM_PROMPT_CACHE_TOKEN_COUNT_OVERRIDE, NON_INFERENCE_CALL_TYPES, OPENAI_EMBEDDING_PARAMS, + OPENAI_SDK_TRANSPORT_PARAMS, PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO, TOOL_CHOICE_OBJECT_TOKEN_COUNT, ) @@ -4818,7 +4819,7 @@ def add_provider_specific_params_to_optional_params( if _should_drop_param(k="extra_body", additional_drop_params=additional_drop_params) is False: extra_body: Final = dict(passed_params.pop("extra_body", None) or {}) for k in passed_params: - if k not in openai_params and passed_params[k] is not None: + if k not in openai_params and k not in OPENAI_SDK_TRANSPORT_PARAMS and passed_params[k] is not None: extra_body[k] = passed_params[k] if not isinstance(optional_params.get("extra_body"), dict): optional_params["extra_body"] = {} @@ -4836,7 +4837,7 @@ def add_provider_specific_params_to_optional_params( optional_params["extra_body"] = _ensure_extra_body_is_safe(extra_body=processed_extra_body) else: for k in passed_params: - if k not in openai_params and passed_params[k] is not None: + if k not in openai_params and k not in OPENAI_SDK_TRANSPORT_PARAMS and passed_params[k] is not None: if _should_drop_param(k=k, additional_drop_params=additional_drop_params): continue optional_params[k] = passed_params[k] diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index c7e46829aba..0a1e5d433eb 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -451,6 +451,25 @@ def test_get_optional_params_image_gen_filters_empty_values(): assert optional_params == {} +def test_get_optional_params_image_gen_excludes_extra_headers_from_extra_body(): + """Fixes https://github.com/BerriAI/litellm/issues/40628: extra_headers must + not fold into extra_body; extra_query still does, with no path elsewhere.""" + from litellm.types.utils import LlmProviders + + provider_config = ProviderConfigManager.get_provider_image_generation_config( + model="gpt-image-1", provider=LlmProviders("openai") + ) + optional_params = get_optional_params_image_gen( + model="gpt-image-1", + custom_llm_provider="openai", + provider_config=provider_config, + drop_params=True, + extra_headers={"cf-aig-authorization": "Bearer cfut_REDACTED"}, + extra_query={"foo": "bar"}, + ) + assert optional_params == {"extra_body": {"extra_query": {"foo": "bar"}}} + + def test_gpt_image_provider_detection_covers_existing_family(): for image_model in ("gpt-image-1", "gpt-image-1-mini", "gpt-image-1.5"): model, custom_llm_provider, _, _ = litellm.get_llm_provider(model=image_model) @@ -3743,6 +3762,59 @@ class TestAdditionalDropParamsForNonOpenAIProviders: assert result.get("custom_param") == "value" +class TestSdkTransportParamsExcludedFromExtraBody: + """Fixes https://github.com/BerriAI/litellm/issues/40628: extra_headers/timeout + must never fold into extra_body pass-through; extra_query still does, since it + has no other forwarding path yet.""" + + def test_excluded_for_openai_family_while_extra_query_and_unknown_still_pass_through(self): + from litellm.utils import add_provider_specific_params_to_optional_params + + passed_params = { + "extra_headers": {"cf-aig-authorization": "Bearer token"}, + "extra_query": {"foo": "bar"}, + "timeout": 30, + "unknown_param": "kept-in-extra-body", + } + openai_params = ["background", "moderation", "n", "size"] + + result = add_provider_specific_params_to_optional_params( + optional_params={}, + passed_params=passed_params, + custom_llm_provider="openai", + openai_params=openai_params, + additional_drop_params=None, + ) + + assert result == { + "extra_body": { + "extra_query": {"foo": "bar"}, + "unknown_param": "kept-in-extra-body", + } + } + + def test_excluded_for_non_openai_family_while_extra_query_and_unknown_still_pass_through(self): + from litellm.utils import add_provider_specific_params_to_optional_params + + passed_params = { + "extra_headers": {"x-custom": "value"}, + "extra_query": {"foo": "bar"}, + "timeout": 30, + "custom_param": "keep_me", + } + openai_params = ["temperature"] + + result = add_provider_specific_params_to_optional_params( + optional_params={}, + passed_params=passed_params, + custom_llm_provider="bedrock", + openai_params=openai_params, + additional_drop_params=None, + ) + + assert result == {"extra_query": {"foo": "bar"}, "custom_param": "keep_me"} + + class TestDropParamsWithPromptCacheKey: """ Test that drop_params: true correctly drops prompt_cache_key for non-OpenAI providers.