From 4e55594a3528c81a6f144b54a48bda8a1f6ab5f2 Mon Sep 17 00:00:00 2001 From: Mika Senghaas Date: Sun, 15 Mar 2026 00:37:33 +0530 Subject: [PATCH 01/13] remove redundant pydantic calls (mostly normalize_messages) --- verifiers/envs/environment.py | 4 +--- verifiers/envs/multiturn_env.py | 38 +++++++++++++------------------ verifiers/utils/response_utils.py | 14 +++++------- 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/verifiers/envs/environment.py b/verifiers/envs/environment.py index 848683418a..89ff1ea878 100644 --- a/verifiers/envs/environment.py +++ b/verifiers/envs/environment.py @@ -539,12 +539,10 @@ def resolve_optional_args( client, model, tool_defs, sampling_args ) - normalized_prompt = normalize_messages(prompt, field_name="prompt") - self._get_usage_tracker(state, create_if_missing=True) response = await client.get_response( - prompt=normalized_prompt, + prompt=prompt, model=model, tools=tool_defs, sampling_args=sampling_args, diff --git a/verifiers/envs/multiturn_env.py b/verifiers/envs/multiturn_env.py index 4e2c867c8b..e316ffe7cd 100644 --- a/verifiers/envs/multiturn_env.py +++ b/verifiers/envs/multiturn_env.py @@ -71,13 +71,9 @@ async def setup_state(self, state: State) -> State: async def get_prompt_messages(self, state: State) -> Messages: """Override for rollouts with non-linear message sequences.""" if len(state["trajectory"]) == 0: - return normalize_messages(state["prompt"], field_name="state.prompt") - prev_turn_prompt = normalize_messages( - state["trajectory"][-1]["prompt"], field_name="trajectory.prompt" - ) - prev_turn_completion = normalize_messages( - state["trajectory"][-1]["completion"], field_name="trajectory.completion" - ) + return state["prompt"] + prev_turn_prompt = state["trajectory"][-1]["prompt"] + prev_turn_completion = state["trajectory"][-1]["completion"] messages = concat_messages([prev_turn_prompt, prev_turn_completion]) env_response = await self.env_response(messages, state) env_response_messages = normalize_messages( @@ -90,23 +86,21 @@ async def render_completion(self, state: State): if len(state["trajectory"]) == 0: state["completion"] = [] return - last_prompt = normalize_messages( - state["trajectory"][-1]["prompt"], field_name="trajectory.prompt" - ) - last_completion = normalize_messages( - state["trajectory"][-1]["completion"], field_name="trajectory.completion" - ) + last_prompt = state["trajectory"][-1]["prompt"] + last_completion = state["trajectory"][-1]["completion"] full_conversation = concat_messages([last_prompt, last_completion]) if state.get("final_env_response"): - full_conversation = concat_messages( - [ - full_conversation, - normalize_messages( - state["final_env_response"], field_name="final_env_response" - ), - ] - ) - prompt_messages = normalize_messages(state["prompt"], field_name="state.prompt") + final_resp = state["final_env_response"] + if isinstance(final_resp, str) or ( + isinstance(final_resp, list) + and final_resp + and isinstance(final_resp[0], dict) + ): + final_resp = normalize_messages( + final_resp, field_name="final_env_response" + ) + full_conversation = concat_messages([full_conversation, final_resp]) + prompt_messages = state["prompt"] state["completion"] = full_conversation[len(prompt_messages) :] async def add_trajectory_step(self, state: State, trajectory_step: TrajectoryStep): diff --git a/verifiers/utils/response_utils.py b/verifiers/utils/response_utils.py index b24f1c2f77..03de5fc2ea 100644 --- a/verifiers/utils/response_utils.py +++ b/verifiers/utils/response_utils.py @@ -9,14 +9,12 @@ async def parse_response_message(response: Response) -> Messages: """Parse a vf.Response into a vf.Messages list (single vf.AssistantMessage).""" response_message = response.message - message_payload = { - "role": "assistant", - "content": response_message.content, - "reasoning_content": response_message.reasoning_content, - "thinking_blocks": response_message.thinking_blocks, - "tool_calls": response_message.tool_calls, - } - message = AssistantMessage.model_validate(message_payload) + message = AssistantMessage( + content=response_message.content, + reasoning_content=response_message.reasoning_content, + thinking_blocks=response_message.thinking_blocks, + tool_calls=response_message.tool_calls, + ) return [message] From 8ee58594efc644da0aa45ea928c043a8fcee3f1e Mon Sep 17 00:00:00 2001 From: Mika Senghaas Date: Tue, 17 Mar 2026 10:31:36 +0000 Subject: [PATCH 02/13] keep normalizer in get_model_response --- verifiers/envs/environment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/verifiers/envs/environment.py b/verifiers/envs/environment.py index 89ff1ea878..848683418a 100644 --- a/verifiers/envs/environment.py +++ b/verifiers/envs/environment.py @@ -539,10 +539,12 @@ def resolve_optional_args( client, model, tool_defs, sampling_args ) + normalized_prompt = normalize_messages(prompt, field_name="prompt") + self._get_usage_tracker(state, create_if_missing=True) response = await client.get_response( - prompt=prompt, + prompt=normalized_prompt, model=model, tools=tool_defs, sampling_args=sampling_args, From eb5a5ca70c339cf8ce7600ac223f313160d5627b Mon Sep 17 00:00:00 2001 From: Mika Senghaas Date: Tue, 17 Mar 2026 10:32:08 +0000 Subject: [PATCH 03/13] Revert "keep normalizer in get_model_response" This reverts commit 8b26a4250a581954a3edbb12268deb4b0ebeeed9. --- verifiers/envs/environment.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/verifiers/envs/environment.py b/verifiers/envs/environment.py index 848683418a..89ff1ea878 100644 --- a/verifiers/envs/environment.py +++ b/verifiers/envs/environment.py @@ -539,12 +539,10 @@ def resolve_optional_args( client, model, tool_defs, sampling_args ) - normalized_prompt = normalize_messages(prompt, field_name="prompt") - self._get_usage_tracker(state, create_if_missing=True) response = await client.get_response( - prompt=normalized_prompt, + prompt=prompt, model=model, tools=tool_defs, sampling_args=sampling_args, From 4bb8a63ed47f62836d19d4cc30465be2b2093a10 Mon Sep 17 00:00:00 2001 From: Mika Senghaas Date: Tue, 17 Mar 2026 10:33:29 +0000 Subject: [PATCH 04/13] fix ty --- verifiers/envs/environment.py | 2 +- verifiers/envs/experimental/cli_agent_env.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/verifiers/envs/environment.py b/verifiers/envs/environment.py index 89ff1ea878..4b91933161 100644 --- a/verifiers/envs/environment.py +++ b/verifiers/envs/environment.py @@ -494,7 +494,7 @@ def get_state_usage(self, state: State) -> TokenUsage | None: async def get_model_response( self, state: State, - prompt: Messages | str, + prompt: Messages, client: Client | None = None, model: str | None = None, tool_defs: list[Tool] | None = None, diff --git a/verifiers/envs/experimental/cli_agent_env.py b/verifiers/envs/experimental/cli_agent_env.py index 1db7005204..8f5d86fb0d 100644 --- a/verifiers/envs/experimental/cli_agent_env.py +++ b/verifiers/envs/experimental/cli_agent_env.py @@ -436,7 +436,7 @@ async def get_prompt_messages(self, state: State) -> Messages: async def get_model_response( self, state: State, - prompt: Messages | str, + prompt: Messages, client: Client | None = None, model: str | None = None, tool_defs: list[Tool] | None = None, From 1877e7b5f81d4624122947a7b71fdd2fcd025392 Mon Sep 17 00:00:00 2001 From: Mika Senghaas Date: Wed, 18 Mar 2026 11:54:43 +0000 Subject: [PATCH 05/13] add warning and auto-normalization --- verifiers/envs/environment.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/verifiers/envs/environment.py b/verifiers/envs/environment.py index 4b91933161..4261f94c3f 100644 --- a/verifiers/envs/environment.py +++ b/verifiers/envs/environment.py @@ -494,7 +494,7 @@ def get_state_usage(self, state: State) -> TokenUsage | None: async def get_model_response( self, state: State, - prompt: Messages, + prompt: Messages | str, client: Client | None = None, model: str | None = None, tool_defs: list[Tool] | None = None, @@ -541,6 +541,18 @@ def resolve_optional_args( self._get_usage_tracker(state, create_if_missing=True) + if not isinstance(prompt, list) or not all( + isinstance(m, vf.Message) for m in prompt + ): + self.logger.warning( + "get_model_response() received raw dicts/strings instead of " + "vf.Message objects. This triggers normalize_messages() on every " + "call, which can cause unnecessary Pydantic validation overhaead. " + "Return vf.Message types (e.g. vf.UserMessage, vf.AssistantMessage) " + "from get_prompt_messages() to avoid this overhead." + ) + prompt = normalize_messages(prompt) + response = await client.get_response( prompt=prompt, model=model, From e219eeb75302668005d57c948a2cb69c54bdeac2 Mon Sep 17 00:00:00 2001 From: Mika Senghaas Date: Wed, 18 Mar 2026 11:56:07 +0000 Subject: [PATCH 06/13] fix ty --- verifiers/envs/experimental/cli_agent_env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verifiers/envs/experimental/cli_agent_env.py b/verifiers/envs/experimental/cli_agent_env.py index 8f5d86fb0d..1db7005204 100644 --- a/verifiers/envs/experimental/cli_agent_env.py +++ b/verifiers/envs/experimental/cli_agent_env.py @@ -436,7 +436,7 @@ async def get_prompt_messages(self, state: State) -> Messages: async def get_model_response( self, state: State, - prompt: Messages, + prompt: Messages | str, client: Client | None = None, model: str | None = None, tool_defs: list[Tool] | None = None, From f7b61318073a31730af617986024dc1caded8086 Mon Sep 17 00:00:00 2001 From: Mika Senghaas Date: Wed, 18 Mar 2026 12:13:44 +0000 Subject: [PATCH 07/13] move normalization into env loop to ensure trajectory list has correct types --- verifiers/envs/environment.py | 14 +------------- verifiers/envs/multiturn_env.py | 30 ++++++++++++++++++++++++------ verifiers/utils/message_utils.py | 5 +++++ 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/verifiers/envs/environment.py b/verifiers/envs/environment.py index 4261f94c3f..4b91933161 100644 --- a/verifiers/envs/environment.py +++ b/verifiers/envs/environment.py @@ -494,7 +494,7 @@ def get_state_usage(self, state: State) -> TokenUsage | None: async def get_model_response( self, state: State, - prompt: Messages | str, + prompt: Messages, client: Client | None = None, model: str | None = None, tool_defs: list[Tool] | None = None, @@ -541,18 +541,6 @@ def resolve_optional_args( self._get_usage_tracker(state, create_if_missing=True) - if not isinstance(prompt, list) or not all( - isinstance(m, vf.Message) for m in prompt - ): - self.logger.warning( - "get_model_response() received raw dicts/strings instead of " - "vf.Message objects. This triggers normalize_messages() on every " - "call, which can cause unnecessary Pydantic validation overhaead. " - "Return vf.Message types (e.g. vf.UserMessage, vf.AssistantMessage) " - "from get_prompt_messages() to avoid this overhead." - ) - prompt = normalize_messages(prompt) - response = await client.get_response( prompt=prompt, model=model, diff --git a/verifiers/envs/multiturn_env.py b/verifiers/envs/multiturn_env.py index e316ffe7cd..c79779bf95 100644 --- a/verifiers/envs/multiturn_env.py +++ b/verifiers/envs/multiturn_env.py @@ -13,7 +13,11 @@ State, TrajectoryStep, ) -from verifiers.utils.message_utils import concat_messages, normalize_messages +from verifiers.utils.message_utils import ( + concat_messages, + normalize_messages, + requires_normalize_messages, +) from verifiers.utils.response_utils import ( parse_response_message, parse_response_tokens, @@ -91,11 +95,14 @@ async def render_completion(self, state: State): full_conversation = concat_messages([last_prompt, last_completion]) if state.get("final_env_response"): final_resp = state["final_env_response"] - if isinstance(final_resp, str) or ( - isinstance(final_resp, list) - and final_resp - and isinstance(final_resp[0], dict) - ): + if requires_normalize_messages(final_resp): + self.logger.warning( + "final_env_response returned raw dicts/strings instead of " + "vf.Message objects. This triggers normalize_messages() on every " + "turn, which can cause unnecessary Pydantic validation overhead. " + "Return vf.Message types (e.g. vf.UserMessage, vf.AssistantMessage) " + "from env_response() to avoid this overhead." + ) final_resp = normalize_messages( final_resp, field_name="final_env_response" ) @@ -150,6 +157,17 @@ async def rollout( while not await self.is_completed(state): try: prompt_messages = await self.get_prompt_messages(state) + if requires_normalize_messages(prompt_messages): + self.logger.warning( + "get_prompt_messages() returned raw dicts/strings instead of " + "vf.Message objects. This triggers normalize_messages() on every " + "turn, which can cause unnecessary Pydantic validation overhead. " + "Return vf.Message types (e.g. vf.UserMessage, vf.AssistantMessage) " + "from get_prompt_messages() to avoid this overhead." + ) + prompt_messages = normalize_messages( + prompt_messages, field_name="prompt_messages" + ) if state.get("final_env_response") is not None: continue response = await self.get_model_response(state, prompt_messages) diff --git a/verifiers/utils/message_utils.py b/verifiers/utils/message_utils.py index b3c55318bd..d0d86f6912 100644 --- a/verifiers/utils/message_utils.py +++ b/verifiers/utils/message_utils.py @@ -139,6 +139,11 @@ def normalize_messages( return normalized +def requires_normalize_messages(value: list | str) -> bool: + """Check if a message list requires normalization into a list of Message objects.""" + return not isinstance(value, list) or not all(isinstance(m, Message) for m in value) + + def concat_messages(messages_list: list[Messages]) -> Messages: """Concatenate multiple Messages lists into one.""" result = [] From e86b558352cfe267f722fa62800d1756a15d21e2 Mon Sep 17 00:00:00 2001 From: Mika Senghaas Date: Wed, 18 Mar 2026 12:31:46 +0000 Subject: [PATCH 08/13] maybe_normalize_messages and updated env_response return type --- verifiers/envs/multiturn_env.py | 36 +++++++++----------------------- verifiers/utils/message_utils.py | 25 +++++++++++++++++++--- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/verifiers/envs/multiturn_env.py b/verifiers/envs/multiturn_env.py index c79779bf95..f8a6c3d4a9 100644 --- a/verifiers/envs/multiturn_env.py +++ b/verifiers/envs/multiturn_env.py @@ -15,8 +15,7 @@ ) from verifiers.utils.message_utils import ( concat_messages, - normalize_messages, - requires_normalize_messages, + maybe_normalize_messages, ) from verifiers.utils.response_utils import ( parse_response_message, @@ -45,7 +44,7 @@ def __init__(self, max_turns: int = -1, **kwargs): @abstractmethod async def env_response( self, messages: Messages, state: State, **kwargs - ) -> Messages | str: + ) -> Messages: """ Generate a response from the environment. """ @@ -80,7 +79,7 @@ async def get_prompt_messages(self, state: State) -> Messages: prev_turn_completion = state["trajectory"][-1]["completion"] messages = concat_messages([prev_turn_prompt, prev_turn_completion]) env_response = await self.env_response(messages, state) - env_response_messages = normalize_messages( + env_response_messages = maybe_normalize_messages( env_response, field_name="env_response" ) return concat_messages([messages, env_response_messages]) @@ -95,17 +94,9 @@ async def render_completion(self, state: State): full_conversation = concat_messages([last_prompt, last_completion]) if state.get("final_env_response"): final_resp = state["final_env_response"] - if requires_normalize_messages(final_resp): - self.logger.warning( - "final_env_response returned raw dicts/strings instead of " - "vf.Message objects. This triggers normalize_messages() on every " - "turn, which can cause unnecessary Pydantic validation overhead. " - "Return vf.Message types (e.g. vf.UserMessage, vf.AssistantMessage) " - "from env_response() to avoid this overhead." - ) - final_resp = normalize_messages( - final_resp, field_name="final_env_response" - ) + final_resp = maybe_normalize_messages( + final_resp, field_name="final_env_response" + ) full_conversation = concat_messages([full_conversation, final_resp]) prompt_messages = state["prompt"] state["completion"] = full_conversation[len(prompt_messages) :] @@ -157,17 +148,10 @@ async def rollout( while not await self.is_completed(state): try: prompt_messages = await self.get_prompt_messages(state) - if requires_normalize_messages(prompt_messages): - self.logger.warning( - "get_prompt_messages() returned raw dicts/strings instead of " - "vf.Message objects. This triggers normalize_messages() on every " - "turn, which can cause unnecessary Pydantic validation overhead. " - "Return vf.Message types (e.g. vf.UserMessage, vf.AssistantMessage) " - "from get_prompt_messages() to avoid this overhead." - ) - prompt_messages = normalize_messages( - prompt_messages, field_name="prompt_messages" - ) + prompt_messages = maybe_normalize_messages( + prompt_messages, + field_name="prompt_messages", + ) if state.get("final_env_response") is not None: continue response = await self.get_model_response(state, prompt_messages) diff --git a/verifiers/utils/message_utils.py b/verifiers/utils/message_utils.py index d0d86f6912..e081b43588 100644 --- a/verifiers/utils/message_utils.py +++ b/verifiers/utils/message_utils.py @@ -1,4 +1,5 @@ import json +import logging import re from collections.abc import Mapping from typing import Any, cast @@ -19,6 +20,8 @@ UserMessage, ) +logger = logging.getLogger(__name__) + def from_raw_content_part(part: dict[str, Any]) -> ContentPart: """Convert a raw content-part dict to a typed content part when possible.""" @@ -139,9 +142,25 @@ def normalize_messages( return normalized -def requires_normalize_messages(value: list | str) -> bool: - """Check if a message list requires normalization into a list of Message objects.""" - return not isinstance(value, list) or not all(isinstance(m, Message) for m in value) +def maybe_normalize_messages( + value: Messages | str, + *, + field_name: str = "messages", +) -> Messages: + """Normalize messages only if needed, logging a warning on first occurrence.""" + requires_normalize = not isinstance(value, list) or not all( + isinstance(m, Message) for m in value + ) + if not requires_normalize: + return cast(Messages, value) + logger.warning( + "%s returned raw dicts/strings instead of vf.Messages. This" + " repeatedly triggers normalize_messages(), causing unnecessary" + " Pydantic validation overhead. Return vf.Message types (e.g." + " vf.UserMessage, vf.AssistantMessage) to avoid this.", + field_name, + ) + return normalize_messages(value, field_name=field_name) def concat_messages(messages_list: list[Messages]) -> Messages: From f1dbfcb12b6886e4acbba2b6f84704fd23f9b8f5 Mon Sep 17 00:00:00 2001 From: Mika Senghaas Date: Wed, 18 Mar 2026 12:35:28 +0000 Subject: [PATCH 09/13] fix envs + api --- environments/alphabet_sort/alphabet_sort.py | 2 +- environments/doublecheck/doublecheck.py | 2 +- environments/sentence_repeater/sentence_repeater.py | 7 +------ verifiers/envs/experimental/cli_agent_env.py | 2 +- verifiers/envs/experimental/gym_env.py | 6 +++--- 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/environments/alphabet_sort/alphabet_sort.py b/environments/alphabet_sort/alphabet_sort.py index 75565121c6..4c5cc1cfef 100644 --- a/environments/alphabet_sort/alphabet_sort.py +++ b/environments/alphabet_sort/alphabet_sort.py @@ -224,7 +224,7 @@ async def env_response( assistant_count = len([m for m in messages if m["role"] == "assistant"]) follow_ups = state["info"]["follow_ups"] follow_up_idx = assistant_count - 1 - return [{"role": "user", "content": follow_ups[follow_up_idx]}] + return [vf.UserMessage(content=follow_ups[follow_up_idx])] def score_response( predicted: List[str], expected: List[str], apply_power: bool = True diff --git a/environments/doublecheck/doublecheck.py b/environments/doublecheck/doublecheck.py index a56b820aa1..bf7162ca7e 100644 --- a/environments/doublecheck/doublecheck.py +++ b/environments/doublecheck/doublecheck.py @@ -27,7 +27,7 @@ async def env_response( self, messages: Messages, state: State, **kwargs ) -> Messages: """Generate a response from the environment.""" - return [{"role": "user", "content": "Are you sure?"}] + return [vf.UserMessage(content="Are you sure?")] def load_environment( diff --git a/environments/sentence_repeater/sentence_repeater.py b/environments/sentence_repeater/sentence_repeater.py index e189cedad9..e7ba1152a2 100644 --- a/environments/sentence_repeater/sentence_repeater.py +++ b/environments/sentence_repeater/sentence_repeater.py @@ -83,12 +83,7 @@ async def env_response( self, messages: Messages, state: State, **kwargs ) -> Messages: num_turn = len(state["trajectory"]) - return [ - { - "role": "user", - "content": state["info"]["questions"][num_turn], - } - ] + return [vf.UserMessage(content=state["info"]["questions"][num_turn])] def load_environment(**kwargs) -> vf.Environment: diff --git a/verifiers/envs/experimental/cli_agent_env.py b/verifiers/envs/experimental/cli_agent_env.py index 1db7005204..8f5d86fb0d 100644 --- a/verifiers/envs/experimental/cli_agent_env.py +++ b/verifiers/envs/experimental/cli_agent_env.py @@ -436,7 +436,7 @@ async def get_prompt_messages(self, state: State) -> Messages: async def get_model_response( self, state: State, - prompt: Messages | str, + prompt: Messages, client: Client | None = None, model: str | None = None, tool_defs: list[Tool] | None = None, diff --git a/verifiers/envs/experimental/gym_env.py b/verifiers/envs/experimental/gym_env.py index fe4781568a..0d0096319b 100644 --- a/verifiers/envs/experimental/gym_env.py +++ b/verifiers/envs/experimental/gym_env.py @@ -142,12 +142,12 @@ def obs_to_text(self, obs: Any) -> str: return self.obs_to_text_fn(obs) return str(obs) - def wrap_response(self, text: str) -> vf.Messages | str: - return cast(vf.Messages, [{"role": "user", "content": text}]) + def wrap_response(self, text: str) -> vf.Messages: + return [vf.UserMessage(content=text)] async def env_response( self, messages: vf.Messages, state: State, **kwargs: Any - ) -> vf.Messages | str: + ) -> vf.Messages: if "gym_env" not in state: env = self.env_cls(**self.env_kwargs) seed = int(state["answer"]) From 559278afcde7049a6e03c51cf7b69980983a641b Mon Sep 17 00:00:00 2001 From: Mika Senghaas Date: Wed, 18 Mar 2026 12:40:01 +0000 Subject: [PATCH 10/13] style --- verifiers/envs/multiturn_env.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/verifiers/envs/multiturn_env.py b/verifiers/envs/multiturn_env.py index f8a6c3d4a9..d43f69d95c 100644 --- a/verifiers/envs/multiturn_env.py +++ b/verifiers/envs/multiturn_env.py @@ -79,10 +79,8 @@ async def get_prompt_messages(self, state: State) -> Messages: prev_turn_completion = state["trajectory"][-1]["completion"] messages = concat_messages([prev_turn_prompt, prev_turn_completion]) env_response = await self.env_response(messages, state) - env_response_messages = maybe_normalize_messages( - env_response, field_name="env_response" - ) - return concat_messages([messages, env_response_messages]) + env_response = maybe_normalize_messages(env_response, field_name="env_response") + return concat_messages([messages, env_response]) async def render_completion(self, state: State): """Override for rollouts with non-linear message sequences.""" @@ -149,8 +147,7 @@ async def rollout( try: prompt_messages = await self.get_prompt_messages(state) prompt_messages = maybe_normalize_messages( - prompt_messages, - field_name="prompt_messages", + prompt_messages, field_name="prompt_messages" ) if state.get("final_env_response") is not None: continue From 162b0ac9e96ef35f56456446b05b5c7c42605c90 Mon Sep 17 00:00:00 2001 From: Mika Senghaas Date: Wed, 18 Mar 2026 13:06:07 +0000 Subject: [PATCH 11/13] log once utils --- verifiers/utils/logging_utils.py | 27 ++++++++++++++++++++++++++- verifiers/utils/message_utils.py | 5 ++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/verifiers/utils/logging_utils.py b/verifiers/utils/logging_utils.py index 6d0084f644..cdd43f780e 100644 --- a/verifiers/utils/logging_utils.py +++ b/verifiers/utils/logging_utils.py @@ -12,10 +12,33 @@ from verifiers.errors import Error from verifiers.types import ErrorInfo, Messages from verifiers.utils.error_utils import ErrorChain -from verifiers.utils.message_utils import format_messages LOGGER_NAME = "verifiers" +_seen_once_keys: set[tuple[str, str]] = set() + + +def log_once( + logger: logging.Logger, + level: int, + msg: str, + *args: object, + **kwargs: object, +) -> None: + """Log a message only once per (logger name, message) pair for the process lifetime.""" + key = (logger.name, msg) + if key in _seen_once_keys: + return + _seen_once_keys.add(key) + logger.log(level, msg, *args, **kwargs) + + +def warning_once( + logger: logging.Logger, msg: str, *args: object, **kwargs: object +) -> None: + """Shorthand for ``log_once(logger, logging.WARNING, ...)``.""" + log_once(logger, logging.WARNING, msg, *args, **kwargs) + class JsonFormatter(logging.Formatter): """JSON formatter for structured logging.""" @@ -144,6 +167,8 @@ def print_prompt_completions_sample( step: int, num_samples: int = 1, ) -> None: + from verifiers.utils.message_utils import format_messages + def format_error(error: ErrorInfo | BaseException) -> Text: out = Text() if isinstance(error, BaseException): diff --git a/verifiers/utils/message_utils.py b/verifiers/utils/message_utils.py index e081b43588..7e82e35534 100644 --- a/verifiers/utils/message_utils.py +++ b/verifiers/utils/message_utils.py @@ -148,12 +148,15 @@ def maybe_normalize_messages( field_name: str = "messages", ) -> Messages: """Normalize messages only if needed, logging a warning on first occurrence.""" + from verifiers.utils.logging_utils import warning_once + requires_normalize = not isinstance(value, list) or not all( isinstance(m, Message) for m in value ) if not requires_normalize: return cast(Messages, value) - logger.warning( + warning_once( + logger, "%s returned raw dicts/strings instead of vf.Messages. This" " repeatedly triggers normalize_messages(), causing unnecessary" " Pydantic validation overhead. Return vf.Message types (e.g." From 74b0b78a34e4077bc066b98c2c1b62a4c3fa0d1d Mon Sep 17 00:00:00 2001 From: Mika Senghaas Date: Wed, 18 Mar 2026 13:39:00 +0000 Subject: [PATCH 12/13] address bugbot --- verifiers/utils/message_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/verifiers/utils/message_utils.py b/verifiers/utils/message_utils.py index 7e82e35534..cdb01a05b3 100644 --- a/verifiers/utils/message_utils.py +++ b/verifiers/utils/message_utils.py @@ -157,11 +157,10 @@ def maybe_normalize_messages( return cast(Messages, value) warning_once( logger, - "%s returned raw dicts/strings instead of vf.Messages. This" + f"{field_name} returned raw dicts/strings instead of vf.Messages. This" " repeatedly triggers normalize_messages(), causing unnecessary" " Pydantic validation overhead. Return vf.Message types (e.g." " vf.UserMessage, vf.AssistantMessage) to avoid this.", - field_name, ) return normalize_messages(value, field_name=field_name) From cd87b2c5ce14ea8b396844fea2220cf9eb78c6ad Mon Sep 17 00:00:00 2001 From: Mika Senghaas Date: Wed, 18 Mar 2026 14:38:29 +0000 Subject: [PATCH 13/13] fix ty --- verifiers/utils/logging_utils.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/verifiers/utils/logging_utils.py b/verifiers/utils/logging_utils.py index cdd43f780e..830f5f3d4a 100644 --- a/verifiers/utils/logging_utils.py +++ b/verifiers/utils/logging_utils.py @@ -18,26 +18,18 @@ _seen_once_keys: set[tuple[str, str]] = set() -def log_once( - logger: logging.Logger, - level: int, - msg: str, - *args: object, - **kwargs: object, -) -> None: +def log_once(logger: logging.Logger, level: int, msg: str) -> None: """Log a message only once per (logger name, message) pair for the process lifetime.""" key = (logger.name, msg) if key in _seen_once_keys: return _seen_once_keys.add(key) - logger.log(level, msg, *args, **kwargs) + logger.log(level, msg) -def warning_once( - logger: logging.Logger, msg: str, *args: object, **kwargs: object -) -> None: +def warning_once(logger: logging.Logger, msg: str) -> None: """Shorthand for ``log_once(logger, logging.WARNING, ...)``.""" - log_once(logger, logging.WARNING, msg, *args, **kwargs) + log_once(logger, logging.WARNING, msg) class JsonFormatter(logging.Formatter):