From 5e67cf2b9f5e62167d48caca31fc892cece5854c Mon Sep 17 00:00:00 2001 From: zunami Date: Wed, 28 Oct 2026 08:20:04 +0100 Subject: [PATCH] fix(guardrails): declare api_base/api_key on GenericGuardrailAPIConfigModel The dashboard renders a provider's configuration form from the fields its config model declares (GET /guardrails/ui/provider_specific_params). For generic_guardrail_api that model only declared optional_params, and the form deliberately skips a nested optional_params block, so selecting the provider rendered no configuration fields at all - not even the api_base the endpoint cannot work without. api_base and api_key are read from litellm_params at runtime, so they never needed to be model fields to work from config.yaml; they do need to be fields for the UI to offer them. Six other providers (compresr, headroom, crowdstrike_aidr, alice, aporia_ai, cato_networks) already declare them this way, so this makes the generic provider consistent rather than inventing a mechanism. Fixes #34927 --- .../guardrail_hooks/generic_guardrail_api.py | 8 ++++++++ .../test_generic_guardrail_api.py | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py b/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py index d38e54986b9..f1bac1becfd 100644 --- a/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py +++ b/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py @@ -130,6 +130,14 @@ class GenericGuardrailAPIConfigModel( ): """Configuration parameters for the Generic Guardrail API guardrail""" + api_base: str | None = Field( + default=None, + description="Base URL of the guardrail endpoint to call (e.g. http://llm-guard:8000/analyze). Required when creating the guardrail: without it there is nowhere to send the request.", + ) + api_key: str | None = Field( + default=None, + description="API key sent to the guardrail endpoint. Optional; omit it when the endpoint does not authenticate the caller.", + ) optional_params: GenericGuardrailAPIOptionalParams | None = Field( default_factory=GenericGuardrailAPIOptionalParams, description="Optional parameters for the Generic Guardrail API guardrail", diff --git a/tests/unit/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py b/tests/unit/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py index 8308a7ba386..69cb6827715 100644 --- a/tests/unit/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py +++ b/tests/unit/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py @@ -1545,6 +1545,26 @@ class TestGenericGuardrailAPIStreamingConfig: assert GenericGuardrailAPI.get_config_model() is GenericGuardrailAPIConfigModel + def test_config_model_declares_connection_fields(self): + """The dashboard builds a provider's form from this model. + + ``get_provider_specific_params`` renders whatever the config model + declares, and the form only offers those keys, so ``api_base`` and + ``api_key`` have to be fields here even though the initializer reads + them off ``litellm_params`` at runtime. Without them the provider shows + no configuration fields at all (BerriAI/litellm#34927). + """ + from litellm.types.proxy.guardrails.guardrail_hooks.generic_guardrail_api import ( + GenericGuardrailAPIConfigModel, + ) + + fields = GenericGuardrailAPIConfigModel.model_json_schema().get("properties", {}) + + for field_name in ("api_base", "api_key"): + assert field_name in fields, f"{field_name} must be a config model field" + assert fields[field_name].get("description"), f"{field_name} needs a description for the form" + assert field_name not in GenericGuardrailAPIConfigModel.model_json_schema().get("required", []) + def test_streaming_transform_mode_defaults_block_only(self): guardrail = GenericGuardrailAPI( api_base="https://api.test.guardrail.com",