diff --git a/docs/my-website/docs/proxy/pass_through.md b/docs/my-website/docs/proxy/pass_through.md index e9c1d796969..f47d7064140 100644 --- a/docs/my-website/docs/proxy/pass_through.md +++ b/docs/my-website/docs/proxy/pass_through.md @@ -58,6 +58,17 @@ Configure the required authentication and pricing: - The Bria API requires an `api_token` header - Enter your Bria API key as the value for the `api_token` header +**Default Query Parameters (Optional):** +- Add query parameters that will be automatically sent with every request +- Perfect for API versioning, format specifications, or default configurations +- Clients can override these parameters by providing their own values +- Example: `version=v1`, `format=json`, `timeout=30` + + + **Pricing Configuration:** - Set a cost per request (e.g., $12.00 in this example) - This enables cost tracking and billing for your users @@ -112,6 +123,9 @@ general_settings: content-type: application/json accept: application/json forward_headers: true # Forward all incoming headers + default_query_params: # Optional: Default query parameters + version: "v1" # Always send version=v1 + format: "json" # Default format (can be overridden) ``` ### Start and Test @@ -167,6 +181,8 @@ general_settings: forward_headers: boolean # Forward all incoming headers include_subpath: boolean # If true, forwards requests to sub-paths (default: false) methods: list[string] # Optional: HTTP methods (e.g., ["GET", "POST"]). If not specified, all methods are supported. + default_query_params: # Optional: Default query parameters sent with every request + : string # Key-value pairs (e.g., version: "v1", format: "json") headers: # Custom headers to add Authorization: string # Auth header for target API content-type: string # Request content type @@ -178,11 +194,17 @@ general_settings: ### Header Options - **Authorization**: Authentication for the target API -- **content-type**: Request body format specification +- **content-type**: Request body format specification - **accept**: Expected response format - **LANGFUSE_PUBLIC_KEY/SECRET_KEY**: For Langfuse integration - **Custom headers**: Any additional key-value pairs +### Default Query Parameters +- **Parameter precedence**: Client params > URL params > default params +- **Use cases**: API versioning, authentication tokens, format control, feature flags +- **Override capability**: Clients can override any default parameter +- **Examples**: `version: "v1"`, `format: "json"`, `timeout: "30"` + ### Sub-path Routing By default, pass-through endpoints only match the **exact path** specified. To forward requests to sub-paths, set `include_subpath: true`: @@ -202,7 +224,51 @@ general_settings: --- -### Method-Specific Routing +### Default Query Parameters + +Pass-through endpoints support default query parameters that are automatically added to every request. This is useful for API versioning, format specifications, authentication tokens, or any default configuration. + +#### How It Works + +**Parameter Precedence (highest to lowest priority):** +1. **Client-provided parameters** (in the request URL) +2. **URL parameters** (from the target URL) +3. **Default parameters** (from configuration) + +#### Example Configuration + +```yaml +general_settings: + pass_through_endpoints: + - path: "/api/v1" + target: "https://external-api.com/service?timeout=60" # URL has timeout=60 + default_query_params: + version: "v1" # Always add version=v1 + format: "json" # Default format=json (can be overridden) + auth_level: "basic" # Always add auth_level=basic +``` + +#### Request Examples + +**Client Request:** `GET /api/v1/users` +**Actual Backend Call:** `https://external-api.com/service?version=v1&format=json&auth_level=basic&timeout=60` + +**Client Request:** `GET /api/v1/users?format=xml&custom=value` +**Actual Backend Call:** `https://external-api.com/service?version=v1&auth_level=basic&timeout=60&format=xml&custom=value` +- Client `format=xml` overrides default `format=json` +- Default `version=v1` and `auth_level=basic` are preserved +- URL `timeout=60` is preserved +- Client `custom=value` is added + +#### Use Cases + +- **API Versioning**: Always send `version=v2` to maintain compatibility +- **Authentication**: Add authentication tokens like `api_key=default_key` +- **Format Control**: Default to `format=json` but allow client override +- **Rate Limiting**: Set `rate_limit=standard` as default +- **Feature Flags**: Enable `experimental=false` by default + +--- You can configure different target URLs for the same path using different HTTP methods. This is useful when different backends handle different operations: diff --git a/docs/my-website/img/passthrough_query_default.png b/docs/my-website/img/passthrough_query_default.png new file mode 100644 index 00000000000..fb97e69001e Binary files /dev/null and b/docs/my-website/img/passthrough_query_default.png differ diff --git a/litellm/passthrough/utils.py b/litellm/passthrough/utils.py index fbbf9cd2581..74af02e5e62 100644 --- a/litellm/passthrough/utils.py +++ b/litellm/passthrough/utils.py @@ -9,7 +9,9 @@ from litellm.constants import PASS_THROUGH_HEADER_PREFIX class BasePassthroughUtils: @staticmethod def get_merged_query_parameters( - existing_url: httpx.URL, request_query_params: Dict[str, Union[str, list]] + existing_url: httpx.URL, + request_query_params: Dict[str, Union[str, list]], + default_query_params: Optional[Dict[str, Union[str, list]]] = None ) -> Dict[str, Union[str, List[str]]]: # Get the existing query params from the target URL existing_query_string = existing_url.query.decode("utf-8") @@ -19,8 +21,19 @@ class BasePassthroughUtils: updated_existing_query_params = { k: v[0] if len(v) == 1 else v for k, v in existing_query_params.items() } - # Merge the query params, giving priority to the existing ones - return {**request_query_params, **updated_existing_query_params} + + # Start with default query params (lowest priority) + merged_params = {} + if default_query_params: + merged_params.update(default_query_params) + + # Override with existing URL query params (medium priority) + merged_params.update(updated_existing_query_params) + + # Override with request query params (highest priority - client can override anything) + merged_params.update(request_query_params) + + return merged_params @staticmethod def forward_headers_from_request( diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index e94e41992d7..2a4fb89fd08 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1909,6 +1909,10 @@ class PassThroughGenericEndpoint(LiteLLMPydanticObjectBase): default={}, description="Key-value pairs of headers to be forwarded with the request. You can set any key value pair here and it will be forwarded to your target endpoint", ) + default_query_params: dict = Field( + default={}, + description="Key-value pairs of default query parameters to be sent with every request to this endpoint. These can be overridden by client-provided query parameters. For example: {'key': 'default_value', 'api_version': '2023-01'}", + ) include_subpath: bool = Field( default=False, description="If True, requests to subpaths of the path will be forwarded to the target endpoint. For example, if the path is /bria and include_subpath is True, requests to /bria/v1/text-to-image/base/2.3 will be forwarded to the target endpoint.", diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 20a446bd29b..76308e08187 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -600,6 +600,7 @@ async def pass_through_request( # noqa: PLR0915 forward_headers: Optional[bool] = False, merge_query_params: Optional[bool] = False, query_params: Optional[dict] = None, + default_query_params: Optional[dict] = None, stream: Optional[bool] = None, cost_per_request: Optional[float] = None, custom_llm_provider: Optional[str] = None, @@ -617,6 +618,7 @@ async def pass_through_request( # noqa: PLR0915 forward_headers: Whether to forward headers merge_query_params: Whether to merge query params query_params: The query params + default_query_params: The default query params to be applied if not overridden by client stream: Whether to stream the response cost_per_request: Optional field - cost per request to the target endpoint custom_llm_provider: Optional field - custom LLM provider for the endpoint @@ -649,13 +651,18 @@ async def pass_through_request( # noqa: PLR0915 forward_headers=forward_headers, ) - if merge_query_params: + # Apply default query parameters if provided, regardless of merge_query_params setting + if default_query_params or merge_query_params: + # Determine what to merge based on settings + request_params = dict(request.query_params) if merge_query_params else {} + # Create a new URL with the merged query params url = url.copy_with( query=urlencode( HttpPassThroughEndpointHelpers.get_merged_query_parameters( existing_url=url, - request_query_params=dict(request.query_params), + request_query_params=request_params, + default_query_params=default_query_params, ) ).encode("ascii") ) @@ -1080,6 +1087,7 @@ def create_pass_through_route( custom_llm_provider: Optional[str] = None, is_streaming_request: Optional[bool] = False, query_params: Optional[dict] = None, + default_query_params: Optional[dict] = None, guardrails: Optional[Dict[str, Any]] = None, ): # check if target is an adapter.py or a url @@ -1171,6 +1179,7 @@ def create_pass_through_route( "cost_per_request", cost_per_request ) param_guardrails = target_params.get("guardrails", None) + param_default_query_params = target_params.get("default_query_params", None) # Construct the full target URL with subpath if needed full_target = ( @@ -1208,6 +1217,7 @@ def create_pass_through_route( forward_headers=cast(Optional[bool], param_forward_headers), merge_query_params=cast(Optional[bool], param_merge_query_params), query_params=final_query_params, + default_query_params=param_default_query_params, stream=is_streaming_request or stream, custom_body=final_custom_body, cost_per_request=cast(Optional[float], param_cost_per_request), @@ -1849,6 +1859,7 @@ class InitPassThroughEndpointHelpers: endpoint_id: str, guardrails: Optional[dict] = None, methods: Optional[List[str]] = None, + default_query_params: Optional[dict] = None, ): """Add exact path route for pass-through endpoint""" # Default to all methods if none specified (backward compatibility) @@ -1886,6 +1897,7 @@ class InitPassThroughEndpointHelpers: merge_query_params, dependencies, cost_per_request=cost_per_request, + default_query_params=default_query_params, guardrails=guardrails, ), methods=methods, @@ -1903,6 +1915,7 @@ class InitPassThroughEndpointHelpers: "custom_headers": custom_headers, "forward_headers": forward_headers, "merge_query_params": merge_query_params, + "default_query_params": default_query_params, "dependencies": dependencies, "cost_per_request": cost_per_request, "guardrails": guardrails, @@ -1922,6 +1935,7 @@ class InitPassThroughEndpointHelpers: endpoint_id: str, guardrails: Optional[dict] = None, methods: Optional[List[str]] = None, + default_query_params: Optional[dict] = None, ): """Add wildcard route for sub-paths""" # Default to all methods if none specified (backward compatibility) @@ -1960,6 +1974,7 @@ class InitPassThroughEndpointHelpers: dependencies, include_subpath=True, cost_per_request=cost_per_request, + default_query_params=default_query_params, guardrails=guardrails, ), methods=methods, @@ -1977,6 +1992,7 @@ class InitPassThroughEndpointHelpers: "custom_headers": custom_headers, "forward_headers": forward_headers, "merge_query_params": merge_query_params, + "default_query_params": default_query_params, "dependencies": dependencies, "cost_per_request": cost_per_request, "guardrails": guardrails, @@ -2170,6 +2186,7 @@ async def initialize_pass_through_endpoints( ) _forward_headers = endpoint.get("forward_headers", None) _merge_query_params = endpoint.get("merge_query_params", None) + _default_query_params = endpoint.get("default_query_params", None) _auth = endpoint.get("auth", None) _dependencies = None if _auth is not None and str(_auth).lower() == "true": @@ -2207,6 +2224,7 @@ async def initialize_pass_through_endpoints( endpoint_id=endpoint_id, guardrails=_guardrails, methods=_methods, + default_query_params=_default_query_params, ) # Generate route key with methods for tracking @@ -2228,6 +2246,7 @@ async def initialize_pass_through_endpoints( endpoint_id=endpoint_id, guardrails=_guardrails, methods=_methods, + default_query_params=_default_query_params, ) visited_endpoints.add(f"{endpoint_id}:subpath:{_path}:{methods_str}") @@ -2546,6 +2565,7 @@ async def update_pass_through_endpoints( endpoint_id=updated_endpoint.id or endpoint_id or "", guardrails=getattr(updated_endpoint, "guardrails", None), methods=updated_endpoint.methods, + default_query_params=updated_endpoint.default_query_params, ) else: InitPassThroughEndpointHelpers.add_exact_path_route( @@ -2560,6 +2580,7 @@ async def update_pass_through_endpoints( endpoint_id=updated_endpoint.id or endpoint_id or "", guardrails=getattr(updated_endpoint, "guardrails", None), methods=updated_endpoint.methods, + default_query_params=updated_endpoint.default_query_params, ) return PassThroughEndpointResponse( @@ -2637,6 +2658,7 @@ async def create_pass_through_endpoints( endpoint_id=created_endpoint.id or "", guardrails=getattr(created_endpoint, "guardrails", None), methods=created_endpoint.methods, + default_query_params=created_endpoint.default_query_params, ) else: InitPassThroughEndpointHelpers.add_exact_path_route( @@ -2651,6 +2673,7 @@ async def create_pass_through_endpoints( endpoint_id=created_endpoint.id or "", guardrails=getattr(created_endpoint, "guardrails", None), methods=created_endpoint.methods, + default_query_params=created_endpoint.default_query_params, ) return PassThroughEndpointResponse(endpoints=[created_endpoint])