From d67353f2888d4b2235a042cf110e14a05f8d163b Mon Sep 17 00:00:00 2001 From: Ashfaq <105435085+Ashfaqbs@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:30:58 +0530 Subject: [PATCH 1/3] fix(image-gen): stop extra_headers/extra_query leaking into the request body add_provider_specific_params_to_optional_params folds any caller kwarg it doesn't recognize into extra_body for pass-through to the provider. For chat completions, extra_headers/extra_query are already part of the recognized param list, so they're excluded automatically. Image generation and audio transcription instead pass the provider config's supported-params list, which has no notion of these SDK transport options, so extra_headers ended up nested inside extra_body and got serialized into the JSON body, producing "Unknown parameter: 'extra_headers'" from OpenAI. Add a shared OPENAI_SDK_TRANSPORT_PARAMS constant and exclude it in both branches of add_provider_specific_params_to_optional_params so this holds for every caller, not just the reported image generation path. Fixes #40628 --- litellm/constants.py | 3 ++ litellm/utils.py | 5 +- tests/test_litellm/test_utils.py | 82 ++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index 028c08a691e..a85177947a2 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"] +# openai-python request-option kwargs: transport-level, never provider request body content +OPENAI_SDK_TRANSPORT_PARAMS: Final = frozenset({"extra_headers", "extra_query", "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 a765e1b1246..dc39a813ebc 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, ) @@ -4804,7 +4805,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"] = {} @@ -4822,7 +4823,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 7753cb7d770..e39f1588ffd 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -437,6 +437,31 @@ 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(): + """https://github.com/BerriAI/litellm/issues/40628 + + extra_headers/extra_query are openai-python SDK transport options, routed as + an actual HTTP request, not model input. GPTImageGenerationConfig's supported + params list has no notion of them, so before the fix they fell into extra_body + pass-through, which the SDK serializes into the JSON body, producing an + "Unknown parameter: 'extra_headers'" 400 from OpenAI. + """ + 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 == {} + + 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) @@ -3729,6 +3754,63 @@ class TestAdditionalDropParamsForNonOpenAIProviders: assert result.get("custom_param") == "value" +class TestSdkTransportParamsExcludedFromExtraBody: + """ + Fixes https://github.com/BerriAI/litellm/issues/40628. + + extra_headers/extra_query/timeout are openai-python SDK transport options, + routed as an actual HTTP request, not provider request-body content. A caller + whose openai_params list is scoped to provider content params only (image + generation, audio transcription) rather than the full chat-completion param + set previously folded them into extra_body pass-through, which the SDK + serializes into the JSON body. + """ + + def test_excluded_for_openai_family_while_unknown_params_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", + } + # mirrors GPTImageGenerationConfig.get_supported_openai_params(), which has + # no notion of SDK transport options + 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": {"unknown_param": "kept-in-extra-body"}} + + def test_excluded_for_non_openai_family_while_unknown_params_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 == {"custom_param": "keep_me"} + + class TestDropParamsWithPromptCacheKey: """ Test that drop_params: true correctly drops prompt_cache_key for non-OpenAI providers. From a13e2985920aad4bec9c421e826b2d02f918ddc7 Mon Sep 17 00:00:00 2001 From: Ashfaq <105435085+Ashfaqbs@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:25:15 +0530 Subject: [PATCH 2/3] fix(image-gen): narrow the extra_body exclusion to extra_headers only extra_query has no existing forwarding path for image generation or audio transcription, unlike extra_headers (headers/optional_params handoff) and timeout (a first-class function argument everywhere). Excluding it from extra_body would trade its current loud failure for a silent no-op instead of fixing anything, so leave it in extra_body as before. Only extra_headers and timeout are excluded now, matching what actually has somewhere correct to go. Updates the regression tests to assert extra_query still lands in extra_body. --- litellm/constants.py | 7 ++++-- tests/test_litellm/test_utils.py | 39 ++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index a85177947a2..ff6291f60ab 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -792,8 +792,11 @@ OPENAI_TRANSCRIPTION_PARAMS: Final = [ OPENAI_EMBEDDING_PARAMS: Final = ["dimensions", "encoding_format", "user"] -# openai-python request-option kwargs: transport-level, never provider request body content -OPENAI_SDK_TRANSPORT_PARAMS: Final = frozenset({"extra_headers", "extra_query", "timeout"}) +# openai-python request-option kwargs that already reach the provider through a working +# path outside extra_body, so they must never also be captured as extra_body content. +# Deliberately excludes extra_query: no such path exists for it yet, so it stays in +# extra_body rather than trading its loud failure for a silent no-op. +OPENAI_SDK_TRANSPORT_PARAMS: Final = frozenset({"extra_headers", "timeout"}) DEFAULT_EMBEDDING_PARAM_VALUES: Final = { **{k: None for k in OPENAI_EMBEDDING_PARAMS}, diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index e39f1588ffd..cf858c9d91b 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -440,11 +440,13 @@ def test_get_optional_params_image_gen_filters_empty_values(): def test_get_optional_params_image_gen_excludes_extra_headers_from_extra_body(): """https://github.com/BerriAI/litellm/issues/40628 - extra_headers/extra_query are openai-python SDK transport options, routed as - an actual HTTP request, not model input. GPTImageGenerationConfig's supported - params list has no notion of them, so before the fix they fell into extra_body + extra_headers is an openai-python SDK transport option, routed as an actual + HTTP header, not model input. GPTImageGenerationConfig's supported params + list has no notion of it, so before the fix it fell into extra_body pass-through, which the SDK serializes into the JSON body, producing an - "Unknown parameter: 'extra_headers'" 400 from OpenAI. + "Unknown parameter: 'extra_headers'" 400 from OpenAI. extra_query has no + working forwarding path for image generation yet, so it's left as before, + still inside extra_body. """ from litellm.types.utils import LlmProviders @@ -459,7 +461,7 @@ def test_get_optional_params_image_gen_excludes_extra_headers_from_extra_body(): extra_headers={"cf-aig-authorization": "Bearer cfut_REDACTED"}, extra_query={"foo": "bar"}, ) - assert optional_params == {} + assert optional_params == {"extra_body": {"extra_query": {"foo": "bar"}}} def test_gpt_image_provider_detection_covers_existing_family(): @@ -3758,15 +3760,17 @@ class TestSdkTransportParamsExcludedFromExtraBody: """ Fixes https://github.com/BerriAI/litellm/issues/40628. - extra_headers/extra_query/timeout are openai-python SDK transport options, - routed as an actual HTTP request, not provider request-body content. A caller - whose openai_params list is scoped to provider content params only (image - generation, audio transcription) rather than the full chat-completion param - set previously folded them into extra_body pass-through, which the SDK - serializes into the JSON body. + extra_headers/timeout already reach the provider through a working path + outside extra_body, so a caller whose openai_params list is scoped to + provider content params only (image generation, audio transcription) rather + than the full chat-completion param set must not also fold them into + extra_body pass-through, which the SDK serializes into the JSON body. + extra_query has no such path yet, so it deliberately keeps landing in + extra_body: still wrong, but a caller gets the same loud failure as before + rather than a new silent no-op. """ - def test_excluded_for_openai_family_while_unknown_params_still_pass_through(self): + 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 = { @@ -3787,9 +3791,14 @@ class TestSdkTransportParamsExcludedFromExtraBody: additional_drop_params=None, ) - assert result == {"extra_body": {"unknown_param": "kept-in-extra-body"}} + assert result == { + "extra_body": { + "extra_query": {"foo": "bar"}, + "unknown_param": "kept-in-extra-body", + } + } - def test_excluded_for_non_openai_family_while_unknown_params_still_pass_through(self): + 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 = { @@ -3808,7 +3817,7 @@ class TestSdkTransportParamsExcludedFromExtraBody: additional_drop_params=None, ) - assert result == {"custom_param": "keep_me"} + assert result == {"extra_query": {"foo": "bar"}, "custom_param": "keep_me"} class TestDropParamsWithPromptCacheKey: From 7f5a2109def902b42c2fb61f0992800ecb891847 Mon Sep 17 00:00:00 2001 From: Ashfaq <105435085+Ashfaqbs@users.noreply.github.com> Date: Fri, 11 Sep 2026 17:14:30 +0530 Subject: [PATCH 3/3] style: trim explanatory comments per repo comment guidance Shortens the extra_body-exclusion rationale in constants.py and the test docstrings to a single line each, keeping only the non-obvious fact worth stating (why extra_query is excluded from the exclusion). --- litellm/constants.py | 5 +---- tests/test_litellm/test_utils.py | 29 +++++------------------------ 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index ff6291f60ab..c72e5ba2dec 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -792,10 +792,7 @@ OPENAI_TRANSCRIPTION_PARAMS: Final = [ OPENAI_EMBEDDING_PARAMS: Final = ["dimensions", "encoding_format", "user"] -# openai-python request-option kwargs that already reach the provider through a working -# path outside extra_body, so they must never also be captured as extra_body content. -# Deliberately excludes extra_query: no such path exists for it yet, so it stays in -# extra_body rather than trading its loud failure for a silent no-op. +# 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 = { diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index cf858c9d91b..6c3d70a2911 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -438,16 +438,8 @@ def test_get_optional_params_image_gen_filters_empty_values(): def test_get_optional_params_image_gen_excludes_extra_headers_from_extra_body(): - """https://github.com/BerriAI/litellm/issues/40628 - - extra_headers is an openai-python SDK transport option, routed as an actual - HTTP header, not model input. GPTImageGenerationConfig's supported params - list has no notion of it, so before the fix it fell into extra_body - pass-through, which the SDK serializes into the JSON body, producing an - "Unknown parameter: 'extra_headers'" 400 from OpenAI. extra_query has no - working forwarding path for image generation yet, so it's left as before, - still inside 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( @@ -3757,18 +3749,9 @@ class TestAdditionalDropParamsForNonOpenAIProviders: class TestSdkTransportParamsExcludedFromExtraBody: - """ - Fixes https://github.com/BerriAI/litellm/issues/40628. - - extra_headers/timeout already reach the provider through a working path - outside extra_body, so a caller whose openai_params list is scoped to - provider content params only (image generation, audio transcription) rather - than the full chat-completion param set must not also fold them into - extra_body pass-through, which the SDK serializes into the JSON body. - extra_query has no such path yet, so it deliberately keeps landing in - extra_body: still wrong, but a caller gets the same loud failure as before - rather than a new silent no-op. - """ + """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 @@ -3779,8 +3762,6 @@ class TestSdkTransportParamsExcludedFromExtraBody: "timeout": 30, "unknown_param": "kept-in-extra-body", } - # mirrors GPTImageGenerationConfig.get_supported_openai_params(), which has - # no notion of SDK transport options openai_params = ["background", "moderation", "n", "size"] result = add_provider_specific_params_to_optional_params(