From 7647a3b70ee4b2f2abc58df89d485f1f795e1835 Mon Sep 17 00:00:00 2001 From: Edoardo Debenedetti Date: Thu, 12 Mar 2026 17:19:47 +0100 Subject: [PATCH 1/3] Add per-LLM generation config overrides to X-Config header Mirrors secure-orchestrator PR #392: the X-Config header now supports an `llm` section for per-LLM generation config overrides (temperature, reasoning, max_completion_tokens, etc.). Co-Authored-By: Claude Opus 4.6 --- src/sequrity/control/__init__.py | 6 ++ src/sequrity/control/types/__init__.py | 6 ++ src/sequrity/control/types/headers.py | 92 +++++++++++++++++++++++++- test/control/test_header_overrides.py | 68 +++++++++++++++++++ 4 files changed, 171 insertions(+), 1 deletion(-) diff --git a/src/sequrity/control/__init__.py b/src/sequrity/control/__init__.py index 56bd36e..5600535 100644 --- a/src/sequrity/control/__init__.py +++ b/src/sequrity/control/__init__.py @@ -38,7 +38,10 @@ FeaturesHeader, FineGrainedConfigHeader, FsmOverrides, + GenerationConfigOverrides, InternalPolicyPresets, + LlmOverrides, + ReasoningConfigOverride, SecurityPolicyHeader, TaggerConfig, ) @@ -54,6 +57,9 @@ "SecurityPolicyHeader", "FineGrainedConfigHeader", "FsmOverrides", + "GenerationConfigOverrides", + "LlmOverrides", + "ReasoningConfigOverride", "TaggerConfig", "ConstraintConfig", "ControlFlowMetaPolicy", diff --git a/src/sequrity/control/types/__init__.py b/src/sequrity/control/types/__init__.py index b7328e6..a8b202c 100644 --- a/src/sequrity/control/types/__init__.py +++ b/src/sequrity/control/types/__init__.py @@ -12,6 +12,9 @@ FeaturesHeader, FineGrainedConfigHeader, FsmOverrides, + GenerationConfigOverrides, + LlmOverrides, + ReasoningConfigOverride, SecurityPolicyHeader, TaggerConfig, ) @@ -23,6 +26,9 @@ "SecurityPolicyHeader", "FineGrainedConfigHeader", "FsmOverrides", + "GenerationConfigOverrides", + "LlmOverrides", + "ReasoningConfigOverride", "TaggerConfig", "ConstraintConfig", "MetaData", diff --git a/src/sequrity/control/types/headers.py b/src/sequrity/control/types/headers.py index 5a32e7a..da80dc9 100644 --- a/src/sequrity/control/types/headers.py +++ b/src/sequrity/control/types/headers.py @@ -609,10 +609,97 @@ class ResponseFormatOverrides(BaseModel): ) +class ReasoningConfigOverride(BaseModel): + """Reasoning configuration for generation config overrides. + + Attributes: + enabled: Whether reasoning/extended thinking is enabled. + effort: Reasoning effort level (e.g., "low", "medium", "high"). + budget: Maximum number of reasoning tokens allowed. + adaptive: When true, the model automatically decides whether to use extended thinking. + """ + + model_config = ConfigDict(extra="forbid") + + enabled: bool = Field(default=True, description="Whether reasoning is enabled.") + effort: str | None = Field(default=None, description="Reasoning effort level (e.g., 'low', 'medium', 'high').") + budget: int | None = Field(default=None, description="Maximum number of reasoning tokens allowed.", ge=1) + adaptive: bool = Field( + default=False, + description="When true, the model automatically decides whether to use extended thinking.", + ) + + +class GenerationConfigOverrides(BaseModel): + """Overrideable generation-config fields (all optional). + + Only fields explicitly set are merged into the target LLM's generation + config; unset fields keep their existing values. + + Attributes: + frequency_penalty: Penalize tokens based on existing frequency (-2.0 to 2.0). + logit_bias: Modify likelihood of specified tokens. + logprobs: Whether to return log probabilities of output tokens. + max_completion_tokens: Upper bound for generated tokens. + presence_penalty: Penalize tokens based on presence in text so far (-2.0 to 2.0). + seed: Seed for deterministic sampling. + temperature: Sampling temperature (0.0 to 2.0). + top_p: Nucleus sampling threshold. + reasoning: Reasoning/extended thinking configuration. + """ + + model_config = ConfigDict(extra="forbid") + + frequency_penalty: float | None = Field(default=None, description="Frequency penalty (-2.0 to 2.0).") + logit_bias: dict[str, int] | None = Field(default=None, description="Token likelihood modifications.") + logprobs: bool | None = Field(default=None, description="Whether to return log probabilities.") + max_completion_tokens: int | None = Field(default=None, description="Upper bound for generated tokens.") + presence_penalty: float | None = Field(default=None, description="Presence penalty (-2.0 to 2.0).") + seed: int | None = Field(default=None, description="Seed for deterministic sampling.") + temperature: float | None = Field(default=None, description="Sampling temperature (0.0 to 2.0).") + top_p: float | None = Field(default=None, description="Nucleus sampling threshold.") + reasoning: ReasoningConfigOverride | None = Field(default=None, description="Reasoning/thinking configuration.") + + +class LlmOverrides(BaseModel): + """Per-LLM generation config overrides. + + Each LLM slot can have its generation parameters overridden independently + via the ``llm`` section of the ``X-Config`` header. If an LLM's override + is ``None``, that LLM's generation config remains unchanged. + + Example: + ```python + from sequrity.control import FineGrainedConfigHeader, LlmOverrides, GenerationConfigOverrides + + config = FineGrainedConfigHeader( + llm=LlmOverrides( + pllm=GenerationConfigOverrides(temperature=0.5, reasoning=ReasoningConfigOverride(enabled=True, effort="high")), + rllm=GenerationConfigOverrides(max_completion_tokens=512), + ), + ) + ``` + """ + + model_config = ConfigDict(extra="forbid") + + pllm: GenerationConfigOverrides | None = Field(default=None, description="Planning LLM generation config.") + rllm: GenerationConfigOverrides | None = Field(default=None, description="Review LLM generation config.") + grllm: GenerationConfigOverrides | None = Field(default=None, description="Grammar-constrained LLM generation config.") + qllm: GenerationConfigOverrides | None = Field(default=None, description="Query LLM generation config.") + tllm: GenerationConfigOverrides | None = Field(default=None, description="Tool-formulating LLM generation config.") + tagllm: GenerationConfigOverrides | None = Field(default=None, description="Tag LLM generation config.") + policy_llm: GenerationConfigOverrides | None = Field(default=None, description="Policy LLM generation config.") + tool_result_error_detector_llm: GenerationConfigOverrides | None = Field( + default=None, description="Tool result error detector LLM generation config." + ) + + class FineGrainedConfigHeader(BaseModel): """Structured configuration header (``X-Config``). - Groups overrides into FSM, prompt, and response format sections. + Groups overrides into FSM, prompt, response format, and per-LLM + generation config sections. Example: ```python @@ -632,6 +719,9 @@ class FineGrainedConfigHeader(BaseModel): response_format: ResponseFormatOverrides | None = Field( default=None, description="Response format configuration for dual-LLM sessions." ) + llm: LlmOverrides | None = Field( + default=None, description="Per-LLM generation config overrides (temperature, reasoning, etc.)." + ) @overload def dump_for_headers(self, mode: Literal["json_str"] = ..., *, overrides: dict[str, Any] | None = ...) -> str: ... diff --git a/test/control/test_header_overrides.py b/test/control/test_header_overrides.py index d2837be..3e2c716 100644 --- a/test/control/test_header_overrides.py +++ b/test/control/test_header_overrides.py @@ -15,6 +15,9 @@ from sequrity.control.types.headers import ( FeaturesHeader, FineGrainedConfigHeader, + GenerationConfigOverrides, + LlmOverrides, + ReasoningConfigOverride, SecurityPolicyHeader, _deep_merge, ) @@ -217,7 +220,72 @@ def test_json_mode_with_overrides(self): assert isinstance(result, dict) assert result["fsm"]["max_n_turns"] == 99 + def test_llm_overrides_serialized(self): + """LLM generation config overrides are included in the header.""" + header = FineGrainedConfigHeader( + llm=LlmOverrides( + pllm=GenerationConfigOverrides(temperature=0.5), + rllm=GenerationConfigOverrides(max_completion_tokens=512), + ), + ) + result = json.loads(header.dump_for_headers()) + assert result["llm"]["pllm"]["temperature"] == 0.5 + assert result["llm"]["rllm"]["max_completion_tokens"] == 512 + + def test_llm_overrides_with_reasoning(self): + """Reasoning config overrides serialize correctly.""" + header = FineGrainedConfigHeader( + llm=LlmOverrides( + pllm=GenerationConfigOverrides( + reasoning=ReasoningConfigOverride(enabled=True, effort="high"), + ), + ), + ) + result = json.loads(header.dump_for_headers()) + assert result["llm"]["pllm"]["reasoning"]["enabled"] is True + assert result["llm"]["pllm"]["reasoning"]["effort"] == "high" + + def test_llm_overrides_multiple_llms(self): + """Multiple LLMs can be overridden independently.""" + header = FineGrainedConfigHeader( + llm=LlmOverrides( + pllm=GenerationConfigOverrides(temperature=0.1), + rllm=GenerationConfigOverrides(reasoning=ReasoningConfigOverride(enabled=True, effort="low")), + grllm=GenerationConfigOverrides(max_completion_tokens=256), + ), + ) + result = json.loads(header.dump_for_headers()) + assert result["llm"]["pllm"]["temperature"] == 0.1 + assert result["llm"]["rllm"]["reasoning"]["effort"] == "low" + assert result["llm"]["grllm"]["max_completion_tokens"] == 256 + # Unset LLMs should not appear + assert "qllm" not in result["llm"] + + def test_llm_overrides_none_excluded(self): + """None llm field is excluded from serialization.""" + header = FineGrainedConfigHeader(llm=None) + result = json.loads(header.dump_for_headers()) + assert "llm" not in result + + def test_llm_overrides_deep_merge_with_dump(self): + """LLM overrides can be deep-merged via dump_for_headers overrides param.""" + header = FineGrainedConfigHeader( + llm=LlmOverrides(pllm=GenerationConfigOverrides(temperature=0.5)), + ) + result = json.loads( + header.dump_for_headers(overrides={"llm": {"pllm": {"temperature": 0.9, "seed": 42}}}) + ) + assert result["llm"]["pllm"]["temperature"] == 0.9 + assert result["llm"]["pllm"]["seed"] == 42 + def test_pydantic_validation_still_strict(self): """extra='forbid' still blocks unknown fields at construction time.""" with pytest.raises(Exception): # ValidationError FineGrainedConfigHeader(fsm=None, bogus_field=True) + + def test_llm_overrides_validation_strict(self): + """extra='forbid' blocks unknown fields on LLM override models.""" + with pytest.raises(Exception): + GenerationConfigOverrides(bogus_field=True) + with pytest.raises(Exception): + LlmOverrides(bogus_llm=GenerationConfigOverrides()) From 13e0fc8336d34a82eb6ffbf14ceccc086bef66ca Mon Sep 17 00:00:00 2001 From: Edoardo Debenedetti Date: Thu, 12 Mar 2026 17:22:47 +0100 Subject: [PATCH 2/3] Apply ruff formatting, drop trivial tests Co-Authored-By: Claude Opus 4.6 --- src/sequrity/control/types/headers.py | 4 +++- test/control/test_header_overrides.py | 17 +---------------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/sequrity/control/types/headers.py b/src/sequrity/control/types/headers.py index da80dc9..c0356f3 100644 --- a/src/sequrity/control/types/headers.py +++ b/src/sequrity/control/types/headers.py @@ -685,7 +685,9 @@ class LlmOverrides(BaseModel): pllm: GenerationConfigOverrides | None = Field(default=None, description="Planning LLM generation config.") rllm: GenerationConfigOverrides | None = Field(default=None, description="Review LLM generation config.") - grllm: GenerationConfigOverrides | None = Field(default=None, description="Grammar-constrained LLM generation config.") + grllm: GenerationConfigOverrides | None = Field( + default=None, description="Grammar-constrained LLM generation config." + ) qllm: GenerationConfigOverrides | None = Field(default=None, description="Query LLM generation config.") tllm: GenerationConfigOverrides | None = Field(default=None, description="Tool-formulating LLM generation config.") tagllm: GenerationConfigOverrides | None = Field(default=None, description="Tag LLM generation config.") diff --git a/test/control/test_header_overrides.py b/test/control/test_header_overrides.py index 3e2c716..cc59f7b 100644 --- a/test/control/test_header_overrides.py +++ b/test/control/test_header_overrides.py @@ -261,20 +261,12 @@ def test_llm_overrides_multiple_llms(self): # Unset LLMs should not appear assert "qllm" not in result["llm"] - def test_llm_overrides_none_excluded(self): - """None llm field is excluded from serialization.""" - header = FineGrainedConfigHeader(llm=None) - result = json.loads(header.dump_for_headers()) - assert "llm" not in result - def test_llm_overrides_deep_merge_with_dump(self): """LLM overrides can be deep-merged via dump_for_headers overrides param.""" header = FineGrainedConfigHeader( llm=LlmOverrides(pllm=GenerationConfigOverrides(temperature=0.5)), ) - result = json.loads( - header.dump_for_headers(overrides={"llm": {"pllm": {"temperature": 0.9, "seed": 42}}}) - ) + result = json.loads(header.dump_for_headers(overrides={"llm": {"pllm": {"temperature": 0.9, "seed": 42}}})) assert result["llm"]["pllm"]["temperature"] == 0.9 assert result["llm"]["pllm"]["seed"] == 42 @@ -282,10 +274,3 @@ def test_pydantic_validation_still_strict(self): """extra='forbid' still blocks unknown fields at construction time.""" with pytest.raises(Exception): # ValidationError FineGrainedConfigHeader(fsm=None, bogus_field=True) - - def test_llm_overrides_validation_strict(self): - """extra='forbid' blocks unknown fields on LLM override models.""" - with pytest.raises(Exception): - GenerationConfigOverrides(bogus_field=True) - with pytest.raises(Exception): - LlmOverrides(bogus_llm=GenerationConfigOverrides()) From 2508cd8876417ff58de0afb875cb10bf04d2a701 Mon Sep 17 00:00:00 2001 From: Edoardo Debenedetti Date: Thu, 12 Mar 2026 17:35:24 +0100 Subject: [PATCH 3/3] Fix description --- src/sequrity/control/types/headers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sequrity/control/types/headers.py b/src/sequrity/control/types/headers.py index c0356f3..8e53cc6 100644 --- a/src/sequrity/control/types/headers.py +++ b/src/sequrity/control/types/headers.py @@ -689,7 +689,7 @@ class LlmOverrides(BaseModel): default=None, description="Grammar-constrained LLM generation config." ) qllm: GenerationConfigOverrides | None = Field(default=None, description="Query LLM generation config.") - tllm: GenerationConfigOverrides | None = Field(default=None, description="Tool-formulating LLM generation config.") + tllm: GenerationConfigOverrides | None = Field(default=None, description="Tool filtering LLM generation config.") tagllm: GenerationConfigOverrides | None = Field(default=None, description="Tag LLM generation config.") policy_llm: GenerationConfigOverrides | None = Field(default=None, description="Policy LLM generation config.") tool_result_error_detector_llm: GenerationConfigOverrides | None = Field(