From 9a627285bc71b07e18df5ca211a93faeb63f3cb8 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 4 Nov 2026 18:54:37 -0800 Subject: [PATCH] feat(proxy): add global raw response model opt-out --- litellm/proxy/_types.py | 8 ++++++++ litellm/proxy/common_request_processing.py | 12 ++++++++---- litellm/proxy/dev_config.yaml | 1 + litellm/proxy/proxy_server.py | 7 ++++++- .../proxy_server/test_streaming_helpers.py | 14 ++++++++++++++ .../proxy/test_common_request_processing.py | 17 +++++++++++++++++ ui/litellm-dashboard/src/lib/http/schema.d.ts | 6 ++++++ 7 files changed, 60 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 1864c3ae82e..df0cc6dd2de 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2480,6 +2480,14 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase): Documents all the fields supported by `general_settings` in config.yaml """ + return_raw_model_name: bool = Field( + False, + description=( + "Preserve the upstream response model instead of replacing it with the requested model alias. " + "Applies to streaming and non-streaming responses. The per-request raw-model opt-out remains supported." + ), + ) + use_spend_logs_ledger: bool = Field( False, description="Apply request accounting from stored SpendLogs deliveries. Enable only after all accounting writers are upgraded and ledger activation gates pass.", diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index e812b03385d..ec35524f52e 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -357,8 +357,10 @@ def resolve_litellm_call_id(client_call_id: str | None) -> str: return str(uuid.uuid4()) -def should_return_raw_model_name(request_data: dict[str, object]) -> bool: - return any( +def should_return_raw_model_name( + request_data: dict[str, object], general_settings: Mapping[str, object] | None = None +) -> bool: + return (general_settings is not None and general_settings.get("return_raw_model_name") is True) or any( isinstance(metadata, dict) and metadata.get(RETURN_RAW_MODEL_NAME_METADATA_KEY) is True for metadata in (request_data.get("metadata"), request_data.get("litellm_metadata")) ) @@ -3314,7 +3316,9 @@ class ProxyBaseLLMRequestProcessing: proxy_logging_obj=proxy_logging_obj, request=request, restamp_model=( - None if should_return_raw_model_name(self.data) else requested_model_from_client + None + if should_return_raw_model_name(self.data, general_settings) + else requested_model_from_client ), ) selected_data_generator = wrap_sse_stream_with_keepalive_pings( @@ -3480,7 +3484,7 @@ class ProxyBaseLLMRequestProcessing: response_obj=response, requested_model=requested_model_from_client, log_context=f"litellm_call_id={logging_obj.litellm_call_id}", - return_raw_model_name=should_return_raw_model_name(self.data), + return_raw_model_name=should_return_raw_model_name(self.data, general_settings), ) fastapi_response.headers.update( diff --git a/litellm/proxy/dev_config.yaml b/litellm/proxy/dev_config.yaml index 1fc9e09b897..f09a5a7b3e7 100644 --- a/litellm/proxy/dev_config.yaml +++ b/litellm/proxy/dev_config.yaml @@ -199,6 +199,7 @@ model_list: general_settings: master_key: os.environ/LITELLM_MASTER_KEY + return_raw_model_name: false # Opt-in: let CheckBatchCost track cost for unmanaged batches created with a raw # gs:// (Vertex) or s3:// (Bedrock) input_file_id. Requires a matching deployment # configured for the batched model. Defaults to false. diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 6a125b9a0a4..c64f5fe01c5 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -10268,8 +10268,9 @@ def _restamp_streaming_chunk_model( model_mismatch_logged: bool, fallback_was_attempted: bool = False, fallback_model_from_metadata: str | None = None, + response_model_settings: Mapping[str, object] | None = None, ) -> tuple[object, bool]: - if should_return_raw_model_name(request_data): + if should_return_raw_model_name(request_data, response_model_settings): return chunk, model_mismatch_logged target_model: Final = fallback_model_from_metadata if fallback_was_attempted else requested_model_from_client @@ -10918,6 +10919,9 @@ async def async_data_generator( model_mismatch_logged=model_mismatch_logged, fallback_was_attempted=fallback_was_attempted, fallback_model_from_metadata=fallback_model_from_metadata, + response_model_settings=cast( # cast-ok: proxy configuration validates the legacy untyped settings global + Mapping[str, object], general_settings + ), ) if strip_stream_usage and is_injected_stream_usage_artifact(chunk): @@ -20592,6 +20596,7 @@ _GENERAL_SETTINGS_CONFIG_LIST_FIELD_TYPES: Final[Mapping[str, str]] = MappingPro "mcp_xff_num_trusted_hops": "Integer", "mcp_prefer_client_id_metadata_document": "Boolean", "always_include_stream_usage": "Boolean", + "return_raw_model_name": "Boolean", "forward_client_headers_to_llm_api": "Boolean", "mcp_required_fields": "List", "mcp_disabled_capabilities": "List", diff --git a/tests/unit/proxy/proxy_server/test_streaming_helpers.py b/tests/unit/proxy/proxy_server/test_streaming_helpers.py index 80bed119e79..2c86ff187a5 100644 --- a/tests/unit/proxy/proxy_server/test_streaming_helpers.py +++ b/tests/unit/proxy/proxy_server/test_streaming_helpers.py @@ -320,6 +320,20 @@ def test_restamp_streaming_chunk_model_respects_raw_model_name_toggle(return_raw assert logged is (not return_raw_model_name) +@pytest.mark.parametrize("global_opt_out", [False, True]) +def test_restamp_streaming_chunk_model_respects_global_raw_model_name(global_opt_out: bool) -> None: + chunk: Final = _simple_chunk(model="provider-model") + new_chunk, logged = _restamp_streaming_chunk_model( + chunk=chunk, + requested_model_from_client="public-alias", + request_data={}, + model_mismatch_logged=False, + response_model_settings={"return_raw_model_name": global_opt_out}, + ) + assert new_chunk.model == ("provider-model" if global_opt_out else "public-alias") + assert logged is (not global_opt_out) + + def test_restamp_streaming_chunk_model_overrides_model_on_dict(): chunk = {"model": "internal", "choices": []} new_chunk, logged = _restamp_streaming_chunk_model( diff --git a/tests/unit/proxy/test_common_request_processing.py b/tests/unit/proxy/test_common_request_processing.py index 8143636a435..372369062bc 100644 --- a/tests/unit/proxy/test_common_request_processing.py +++ b/tests/unit/proxy/test_common_request_processing.py @@ -3277,6 +3277,23 @@ class TestOverrideOpenAIResponseModel: def test_raw_model_name_toggle_metadata(self, request_data, expected): assert should_return_raw_model_name(request_data) is expected + @pytest.mark.parametrize("global_opt_out", [False, True]) + @pytest.mark.parametrize("request_opt_out", [False, True]) + def test_global_raw_model_name_preserves_upstream_response( + self, global_opt_out: bool, request_opt_out: bool + ) -> None: + response_obj: Final = {"model": "provider-model"} + _override_openai_response_model( + response_obj=response_obj, + requested_model="public-alias", + log_context="test_context", + return_raw_model_name=should_return_raw_model_name( + {"litellm_metadata": {RETURN_RAW_MODEL_NAME_METADATA_KEY: request_opt_out}}, + {"return_raw_model_name": global_opt_out}, + ), + ) + assert response_obj["model"] == ("provider-model" if global_opt_out or request_opt_out else "public-alias") + def test_override_model_preserves_fallback_model_when_fallback_occurred_object( self, ): diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index e65a30f6e1d..13055220e4a 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -30799,6 +30799,12 @@ export interface components { * @default 3600 */ responses_websocket_session_limit_seconds: number; + /** + * Return Raw Model Name + * @description Preserve the upstream response model instead of replacing it with the requested model alias. Applies to streaming and non-streaming responses. The per-request raw-model opt-out remains supported. + * @default false + */ + return_raw_model_name: boolean; /** * Return Wildcard Routes * @description When true, `/v1/models` lists wildcard routes like `openai/*` unless the request passes `return_wildcard_routes=false`.