Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/sequrity/control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@
FeaturesHeader,
FineGrainedConfigHeader,
FsmOverrides,
GenerationConfigOverrides,
InternalPolicyPresets,
LlmOverrides,
ReasoningConfigOverride,
SecurityPolicyHeader,
TaggerConfig,
)
Expand All @@ -54,6 +57,9 @@
"SecurityPolicyHeader",
"FineGrainedConfigHeader",
"FsmOverrides",
"GenerationConfigOverrides",
"LlmOverrides",
"ReasoningConfigOverride",
"TaggerConfig",
"ConstraintConfig",
"ControlFlowMetaPolicy",
Expand Down
6 changes: 6 additions & 0 deletions src/sequrity/control/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
FeaturesHeader,
FineGrainedConfigHeader,
FsmOverrides,
GenerationConfigOverrides,
LlmOverrides,
ReasoningConfigOverride,
SecurityPolicyHeader,
TaggerConfig,
)
Expand All @@ -23,6 +26,9 @@
"SecurityPolicyHeader",
"FineGrainedConfigHeader",
"FsmOverrides",
"GenerationConfigOverrides",
"LlmOverrides",
"ReasoningConfigOverride",
"TaggerConfig",
"ConstraintConfig",
"MetaData",
Expand Down
94 changes: 93 additions & 1 deletion src/sequrity/control/types/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,10 +609,99 @@ 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 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(
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
Expand All @@ -632,6 +721,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: ...
Expand Down
53 changes: 53 additions & 0 deletions test/control/test_header_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
from sequrity.control.types.headers import (
FeaturesHeader,
FineGrainedConfigHeader,
GenerationConfigOverrides,
LlmOverrides,
ReasoningConfigOverride,
SecurityPolicyHeader,
_deep_merge,
)
Expand Down Expand Up @@ -217,6 +220,56 @@ 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_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
Expand Down
Loading