Skip to content
Closed
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
11 changes: 6 additions & 5 deletions verifiers/v1/clients/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@
from typing import Any, ClassVar, TypeVar

from openai import OpenAIError
from renderers import OverlongPromptError as RendererOverlongPromptError
from renderers import RenderedTokens, Renderer, RendererConfig
from renderers import OverlongPromptError, RenderedTokens, Renderer, RendererConfig
from renderers.base import ToolCallParseStatus, is_multimodal

from verifiers.v1.clients.base import build_async_openai
from verifiers.v1.clients.client import SESSION_ID_HEADER, Client
from verifiers.v1.configs.client import TrainClientConfig
from verifiers.v1.dialects import FINISH_REASONS, ChatDialect, Dialect, parse_tools
from verifiers.v1.dialects.chat import message_to_wire
from verifiers.v1.errors import OverlongPromptError, model_error
from verifiers.v1.errors import ProviderError, model_error
from verifiers.v1.graph import PendingTurn
from verifiers.v1.types import (
AssistantMessage,
Expand Down Expand Up @@ -436,8 +435,10 @@ def bridge():
if session_id
else None,
)
except RendererOverlongPromptError as e:
raise OverlongPromptError(str(e)) from e
except OverlongPromptError as e:
# The renderer's pre-flight overflow never reached the provider: a
# deterministic 400, so the harness SDK never retries it.
raise ProviderError(str(e), status_code=400) from e
except OpenAIError as e:
raise model_error(e) from e
response = response_from_generate(
Expand Down
8 changes: 2 additions & 6 deletions verifiers/v1/dialects/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
parse_sse_event,
provider_allowed_domains,
)
from verifiers.v1.errors import OverlongPromptError, model_error
from verifiers.v1.errors import model_error
from verifiers.v1.types import (
AssistantMessage,
ContentPart,
Expand Down Expand Up @@ -322,15 +322,11 @@ def response_from_wire(response: OpenAIResponse) -> Response:
code = error.get("code") if isinstance(error, dict) else None
message = error.get("message") if isinstance(error, dict) else None
detail = ": ".join(str(value) for value in (status, code, message) if value)
if code == "context_length_exceeded":
raise OverlongPromptError(
f"upstream Responses request did not complete: {detail}"
)
status_code = (
429
if code in ("rate_limit_exceeded", "rate_limit_error")
else 400
if code == "invalid_prompt"
if code in ("invalid_prompt", "context_length_exceeded")
else 502
)
raise model_error(
Expand Down
43 changes: 4 additions & 39 deletions verifiers/v1/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,6 @@ def __init__(self, message: str = "", *, status_code: int = 502) -> None:
self.status_code = status_code


class OverlongPromptError(ProviderError):
"""The prompt exceeded the model's context window — a budget limit, ended as a clean
truncation rather than recorded as an error. Defaults to a 400 (what the interception
server surfaces for it — deterministic, so an SDK never retries it); `model_error`
keeps the provider's real status when the failure carried one."""

def __init__(self, message: str = "", *, status_code: int = 400) -> None:
super().__init__(message, status_code=status_code)


class HarnessError(RolloutError):
"""The harness failed to install or launch, or its agent process exited unsuccessfully."""

Expand Down Expand Up @@ -99,20 +89,6 @@ async def boundary(error_cls: type[RolloutError], what: str) -> AsyncIterator[No
raise error_cls(f"{what}: {type(e).__name__}: {e}") from e


_CONTEXT_LENGTH_PHRASES = (
"this model's maximum context length is",
"is longer than the model's context length",
"is longer than the maximum model length",
"exceeds the model's context length",
"exceed the configured limit",
"exceeds the configured limit",
"exceeded model",
"prompt_too_long",
"context length",
"maximum model length",
)


def _provider_status(e: OpenAIError | str) -> int:
"""The HTTP status to surface for an SDK error: the provider's own for an HTTP status error, a
retryable 5xx for a transport/timeout fault, else 502."""
Expand All @@ -130,23 +106,12 @@ def _provider_status(e: OpenAIError | str) -> int:
def model_error(
e: OpenAIError | str, *, status_code: int | None = None
) -> ProviderError:
"""Map a provider failure to our error type: an overlong prompt (a budget limit the interception
server turns into a clean truncation) is told apart from any other provider call failure, which
becomes a plain `ProviderError`. `status_code` is the HTTP status surfaced to the harness (whose
SDK then retries 5xx/429/timeout and not 4xx); derived from an SDK error when not given. Accepts
an SDK error (the renderer) or the provider's raw error body (the httpx proxy)."""
from openai import APIStatusError

"""Map a provider failure to a `ProviderError`. `status_code` is the HTTP status surfaced to
the harness (whose SDK then retries 5xx/429/timeout and not 4xx); derived from an SDK error
when not given. Accepts an SDK error (the renderer) or the provider's raw error body (the
httpx proxy)."""
# Some SDK errors stringify empty; fall back to the type so the message is never blank.
text = str(e) or (type(e).__name__ if isinstance(e, BaseException) else "")
if any(phrase in text.casefold() for phrase in _CONTEXT_LENGTH_PHRASES):
# Keep the provider's real status when the failure carried one; else the class
# default (the 400 the interception server surfaces for overlong prompts).
if status_code is None and isinstance(e, APIStatusError):
status_code = e.status_code
return OverlongPromptError(
text, **({} if status_code is None else {"status_code": status_code})
)
return ProviderError(
text,
status_code=status_code if status_code is not None else _provider_status(e),
Expand Down
19 changes: 19 additions & 0 deletions verifiers/v1/harnesses/bash/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
import os
from pathlib import Path

from pydantic import PositiveInt
from pydantic_config import BaseConfig

from verifiers.v1.clients import ModelContext
from verifiers.v1.configs.harness import HarnessConfig
from verifiers.v1.dialects.chat import message_to_wire
Expand All @@ -27,6 +30,14 @@
)


class CompactionConfig(BaseConfig):
"""Context compaction policy for the bash agent loop."""

summarize_at_tokens: PositiveInt | None = None
"""Compact at this token count. When unset, use 90% of the model context window when
the provider advertises it."""


class BashHarnessConfig(HarnessConfig):
edit: bool = True
"""Offer the local `edit` tool (single-occurrence string replacement in a file) alongside
Expand All @@ -37,6 +48,9 @@ class BashHarnessConfig(HarnessConfig):
eval environment; the key is handed to the program over argv (like the interception secret) so
the agent's `bash` subprocesses don't inherit it."""

compaction: CompactionConfig | None = None
"""Context compaction policy. Set an empty config to use automatic thresholds."""


class BashHarness(Harness[BashHarnessConfig]):
APPENDS_SYSTEM_PROMPT = True
Expand Down Expand Up @@ -77,6 +91,11 @@ async def launch(
]
if tool_interception_url:
args.append(f"--tool-interception-url={tool_interception_url}")
if self.config.compaction is not None:
args.append("--compaction")
threshold = self.config.compaction.summarize_at_tokens
if threshold is not None:
args.append(f"--summarize-at-tokens={threshold}")
if self.config.edit:
args.append("--edit")
if self.config.search:
Expand Down
Loading
Loading