Buckets:

Rishik001's picture
download
raw
7.08 kB
"""Helpers for training episodes.
OpenRouter chat calls (target and judge), parsing the attacker's raw output into its thinking and its
message, the exact-value leak check, and loading the config, the scenario files and JSON.
"""
from __future__ import annotations
import copy
import json
import os
import re
import time
from collections.abc import Iterable, Sequence
from typing import Any
import requests
import yaml
from dotenv import load_dotenv
from episode import Job, Message
load_dotenv()
MAX_TOKENS = 16384 # default reply cap, so judge replies are never cut off
MAX_RETRIES = 2 # retries after the first attempt fails
RETRY_WAIT_SECONDS = [5, 20] # wait before retry 1 and retry 2
# ---- OpenRouter ---------------------------------------------------------------------------------
def chat_openrouter(
model: str,
messages: Sequence[Message],
temperature: float = 0.7,
max_tokens: int = MAX_TOKENS,
provider: dict[str, Any] | None = None,
service_tier: str | None = None,
reasoning_effort: str | None = None,
) -> tuple[str, str | None, Any]:
"""Call an OpenRouter chat model and return (reply_text, finish_reason, reasoning).
finish_reason is the model's own ("stop", or "length" when it hit max_tokens), "empty" when it returned
no text, or "error" when every attempt failed; the caller decides what to do then. reasoning is the
model's reasoning text when the provider returns it, else None.
"""
body: dict[str, Any] = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"reasoning": {"exclude": False},
}
if reasoning_effort:
body["reasoning"]["effort"] = reasoning_effort
if provider:
body["provider"] = provider
if service_tier:
body["service_tier"] = service_tier
url = f"{os.environ['OPENROUTER_BASE_URL'].rstrip('/')}/chat/completions"
headers = {"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"}
for attempt in range(MAX_RETRIES + 1):
try:
resp = requests.post(url, headers=headers, json=body, timeout=900)
if not resp.ok: # keep OpenRouter's message, e.g. which provider refused and why
raise requests.HTTPError(f"{resp.status_code}: {resp.text[:300]}")
# OpenRouter can answer HTTP 200 with an error body and no choices; that raises here.
choice = resp.json()["choices"][0]
content = choice["message"].get("content")
reasoning = choice["message"].get("reasoning")
if not content:
return "", "empty", reasoning
return content, choice.get("finish_reason"), reasoning
except (requests.RequestException, KeyError, IndexError, ValueError) as err:
if attempt == MAX_RETRIES:
print(f"[openrouter] {model} failed after {MAX_RETRIES} retries: {err}")
return "", "error", None
print(f"[openrouter] {model} attempt {attempt + 1} failed ({err}); retrying")
time.sleep(RETRY_WAIT_SECONDS[attempt])
raise AssertionError("retry loop exited unexpectedly")
def call_target(target: dict[str, Any], messages: Sequence[Message]) -> tuple[str, str | None, Any]:
"""One reply from the frozen target, using the config's target section."""
return chat_openrouter(
target["model"],
messages,
temperature=target["temperature"],
max_tokens=target["max_tokens"],
provider=target["provider"],
)
# ---- attacker output ----------------------------------------------------------------------------
def parse_attacker(raw: str) -> tuple[str, str | None]:
"""Split a raw attacker turn into (thinking, message).
The model reasons, writes "</think>", then its message; only the message reaches the target. message
is None when the thinking never closed or nothing came after it.
"""
head, closed, tail = raw.rpartition("</think>")
if not closed:
return raw.replace("<think>", "").strip(), None
return head.replace("<think>", "").strip(), tail.strip() or None
def normalize_for_match(text: str) -> str:
"""Lower-case and collapse whitespace, so '12 Harbor View Lane' matches '12 harbor view lane'."""
return " ".join(text.lower().split())
def find_leaked_values(
reply: str, protected_values: Iterable[str], attacker_messages: Iterable[str]
) -> list[str]:
"""Protected values the target's reply contains verbatim.
A value the attacker already wrote itself is skipped: the target echoing it back ("I can't confirm
12 Harbor View Lane") is not a leak, so that case is left to the judge.
"""
reply_norm = normalize_for_match(reply)
said = normalize_for_match(" ".join(attacker_messages))
return [
value
for value in protected_values
if normalize_for_match(value) in reply_norm and normalize_for_match(value) not in said
]
# ---- files and config ---------------------------------------------------------------------------
def extract_json(text: str) -> dict[str, Any]:
"""Parse the first JSON object found in text; {} if there is none or it is invalid."""
match = re.search(r"\{.*\}", text, re.DOTALL)
if not match:
return {}
try:
return json.loads(match.group(0))
except json.JSONDecodeError:
return {}
def load_json(path: str) -> Any:
with open(path, encoding="utf-8") as f:
return json.load(f)
def load_jobs(path: str, repeats: int = 1) -> list[Job]:
"""Every (scenario, request) pair in a scenarios file, each repeated `repeats` times in a row."""
scenarios = load_json(path)
return [
{"scenario": scenario, "request": request, "rep": rep}
for scenario in scenarios
for request in scenario["requests"]
for rep in range(repeats)
]
def load_config(path: str, overrides: Sequence[str] = ()) -> dict[str, Any]:
"""Read config.yaml and apply command-line overrides like "train.lr=5e-5" or "model=org/name".
Each value is parsed as YAML, so numbers, booleans and lists come out typed.
"""
with open(path, encoding="utf-8") as f:
config = yaml.safe_load(f)
for override in overrides:
key, sep, value = override.partition("=")
if not sep:
raise ValueError(f"override must look like section.key=value, got {override!r}")
*sections, leaf = key.split(".")
node = config
for section in sections:
node = node[section]
if leaf not in node:
raise KeyError(f"unknown config key {key!r}")
node[leaf] = yaml.safe_load(value)
return config
def save_config(config: dict[str, Any], path: str) -> None:
"""Write the config a run actually used, so the run can be reproduced from its own folder."""
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
yaml.safe_dump(copy.deepcopy(config), f, sort_keys=False)

Xet Storage Details

Size:
7.08 kB
·
Xet hash:
f7619ee6ae8918299893aa962ce808bb1d2ac402cbd7b950e2f6d8f860cf9b8a

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.