interlens.participant.participants¶
interlens.participant.participants
¶
APIParticipant
dataclass
¶
APIParticipant(
name: str = "",
model_id: str = "",
provider: Provider = "anthropic",
system_prompt: str | None = None,
private_context: tuple = (),
max_tokens: int = 512,
temperature: float = 1.0,
batch: bool = False,
client: object = None,
requires_alternating_roles: bool = True,
)
Bases: Functional, Participant
A participant backed by a hosted API — Claude via anthropic (provider="anthropic", the default) or
any model behind OpenRouter (provider="openrouter", OpenAI-compatible) — for use as a debate opponent,
moderator, or the classifier inside an analyze callback.
It is a full participant for conversation purposes but has no local model — so there is no device,
no activations, and no steering. Any interp request (capture/steering/patch/return_logprobs)
raises rather than silently no-op'ing: in a measurement harness, a steering sweep that quietly did
nothing on an API participant would produce a false "no effect" conclusion. Seeds don't bind hosted models,
so API turns are excluded from the identical-replay guarantee.
Concurrency is network-bound, so pure-API conversations run thread-pooled rather than process-per-GPU
(handled by the runner). The client callable is injectable for testing.
generate_batch
¶
generate_batch(
views: list[list[dict]],
*,
turn: int | None = None,
group_seed: int | None = None,
max_new_tokens: int | None = None
) -> list[Message]
Generate one turn for many independent conversations at once — the API analogue of
ModelParticipant.generate_batch, driven by the runner's co-stepper (rollout(..., batched=True)) to
make large API rollouts cheap and throughput-bound.
With batch=True every view is sent as one asynchronous provider batch (Anthropic Message Batches /
OpenAI Batch API) via client.submit_batch — ~50% cost and much higher throughput, at the price of
batch-window latency. If the provider has no batch API (e.g. OpenRouter) this raises rather than
silently degrading, so a requested batch is never quietly run as serial calls. With batch=False it
falls back to sequential per-view calls (correct, just no batch discount). Interp is unavailable here, as
for generate. turn/group_seed are accepted for co-stepper compatibility but unused (seeds do
not bind hosted models). metadata['batched'] marks these turns.
Source code in src/interlens/participant/participants/api_participant.py
GemmaModelParticipant
dataclass
¶
GemmaModelParticipant(
name: str = "",
hf_id: str | None = None,
weights_path: str | None = None,
dtype: str = "bfloat16",
attn: str = "flash_attention_2",
quant: str | None = None,
revision: str | None = None,
device: str | device | None = None,
max_new_tokens: int = 512,
temperature: float = 0.8,
top_p: float = 0.95,
seed: int | None = None,
thinking: bool | str = "auto",
system_prompt: str | None = None,
private_context: tuple = (),
tools: tuple = (),
max_tool_iters: int = 4,
kv_reuse: bool | str = "auto",
steering: object = None,
_model: "PreTrainedModel | None" = None,
_tokenizer: "PreTrainedTokenizerBase | None" = None,
)
Bases: ModelParticipant
A Gemma-family participant. The only thing that differs from base is the tool-call format
(```tool_code); the chat-template flags (Gemma 2 rejects a standalone system role and requires
strict user/model alternation, Gemma 3 accepts a system role) are auto-derived from the tokenizer's own
template, so both generations use this one class.
parse_tool_calls
¶
Parse Gemma's ```tool_code function-call blocks (best-effort).
Gemma emits calls like ```` ```tool_code
name(arg="x", n=2)
` ```` rather than Hermes JSON. We extract
the block, then the function name and simple keyword arguments. Falls back to[]`` (treat as final
message) on anything it can't parse, matching the base contract of never misfiring silently.
Source code in src/interlens/participant/participants/gemma.py
LlamaModelParticipant
dataclass
¶
LlamaModelParticipant(
name: str = "",
hf_id: str | None = None,
weights_path: str | None = None,
dtype: str = "bfloat16",
attn: str = "flash_attention_2",
quant: str | None = None,
revision: str | None = None,
device: str | device | None = None,
max_new_tokens: int = 512,
temperature: float = 0.8,
top_p: float = 0.95,
seed: int | None = None,
thinking: bool | str = "auto",
system_prompt: str | None = None,
private_context: tuple = (),
tools: tuple = (),
max_tool_iters: int = 4,
kv_reuse: bool | str = "auto",
steering: object = None,
_model: "PreTrainedModel | None" = None,
_tokenizer: "PreTrainedTokenizerBase | None" = None,
)
Bases: ModelParticipant
A Llama-family participant. Chat-template flags are auto-derived from the tokenizer; only the tool-call
format differs from base: Llama 3 emits calls as <|python_tag|>{json} rather than Hermes/Qwen
<tool_call> blocks.
parse_tool_calls
¶
Parse Llama-3's <|python_tag|>{json} function-call format (best-effort).
Everything after <|python_tag|> is one or more JSON objects (separated by newlines or semicolons),
each {"name": ..., "arguments"/"parameters": {...}}. Anything unparseable is skipped, so a malformed
call yields [] (treated as a final message), matching the base contract of never misfiring.
Source code in src/interlens/participant/participants/llama.py
ModelParticipant
dataclass
¶
ModelParticipant(
name: str = "",
hf_id: str | None = None,
weights_path: str | None = None,
dtype: str = "bfloat16",
attn: str = "flash_attention_2",
quant: str | None = None,
revision: str | None = None,
device: str | device | None = None,
max_new_tokens: int = 512,
temperature: float = 0.8,
top_p: float = 0.95,
seed: int | None = None,
thinking: bool | str = "auto",
system_prompt: str | None = None,
private_context: tuple = (),
tools: tuple = (),
max_tool_iters: int = 4,
kv_reuse: bool | str = "auto",
steering: object = None,
_model: "PreTrainedModel | None" = None,
_tokenizer: "PreTrainedTokenizerBase | None" = None,
)
Bases: Functional, Participant
A conversation participant backed by a local HuggingFace causal LM.
Generation flow: the Conversation hands us a view — the transcript rendered from our perspective,
already context-fitted and flattened by finalize_view into [{role, content}]. We apply this model's
own chat template to it, generate, decode only the newly produced tokens, and split any <think>
reasoning out of the visible content. Only the visible answer becomes Message.content;
the parsed reasoning and raw completion live in metadata under neutral keys, so hidden generated text is
never fed back into other participants' views (it's stripped from history automatically because
render_roles uses content).
The participant is its own recipe. It stores what to load (hf_id + dtype/attn/quant/revision) and
loads the weights lazily on first use through the process-wide model cache — so an unrun participant is
cheap (KBs), pickles across a spawn boundary without shipping weights, and .set(...) copies share one
loaded model object by reference (the co-stepping batch win). model/tokenizer are properties that
trigger the load; the raw model is exposed deliberately (interp experiments register forward hooks on it).
Weight loads are keyed on (hf_id, device, dtype, attn, quant, revision) and cached, so all same-recipe
participants on one device share ONE object. Build via from_pretrained (lazy) or from_model (eager,
from an already-loaded model); .set() (from Functional) makes copy-on-write clones with fresh KV state.
model
property
¶
The loaded HF model, loading it on first access (cached process-wide). The raw model is exposed so interp experiments can register forward hooks directly.
tokenizer
property
¶
The loaded tokenizer, loading the model+tokenizer on first access (cached process-wide).
batch_signature
¶
Key identifying which model this participant would batch AS (used by the co-stepper). When loaded, the cached model object's identity is authoritative; when not yet loaded, the load recipe is (the cache guarantees same recipe on one device -> same object), so participants can be grouped WITHOUT forcing a load.
Source code in src/interlens/participant/participants/model_participant.py
for_model_type
classmethod
¶
The registered participant class for a transformers config.model_type (e.g. qwen2, gemma2),
falling back to base ModelParticipant for any family that declares no specialized subclass.
Source code in src/interlens/participant/participants/model_participant.py
from_model
classmethod
¶
from_model(
model: PreTrainedModel,
tokenizer: PreTrainedTokenizerBase | None = None,
*,
name: str,
device: str | device | None = None,
**participant_kwargs
) -> Self
Build a participant of THIS class from an already-loaded model (the eager path — e.g. to wrap a model
you already hold, or share weights between speakers). tokenizer is optional — when omitted it is inferred
from model.config._name_or_path. The chat-template flags (supports_system_role /
requires_alternating_roles) are derived from the tokenizer's own template. hf_id/dtype are read
back from the model so the participant can still be pickled + re-loaded on a spawn worker. Because it
constructs cls, calling it on a subclass returns that subclass — use AutoModelParticipant.from_model
to have the family resolved from config.model_type.
Source code in src/interlens/participant/participants/model_participant.py
from_pretrained
classmethod
¶
from_pretrained(
id_or_path: str | Path,
*,
name: str,
device: str | device = "cuda",
load_kwargs: dict | None = None,
**participant_kwargs
) -> Self
Build a participant of THIS class that will load id_or_path (an HF id or local path) lazily on first
use — no weights are touched here. load_kwargs (dtype / attn / quant / revision /
weights_path) are recorded for that deferred load; participant_kwargs (temperature,
max_new_tokens, system_prompt, tools, kv_reuse, …) go to the participant. When the load fires
it is process-cached, so all same-(id, device, dtype, …) participants share ONE model object. As with
from_model, the class is cls — use AutoModelParticipant.from_pretrained to resolve the family
from config.model_type (it reads only the config, still no weights).
Source code in src/interlens/participant/participants/model_participant.py
generate_batch
¶
generate_batch(
views: list[list[dict]],
*,
turn: int | None = None,
group_seed: int | None = None,
max_new_tokens: int | None = None
) -> list[Message]
Batched generation for many independent conversations that share THIS model (throughput mode).
Renders each view with this model's chat template and runs one model.generate over the
left-padded batch, returning one Message per view — the co-stepping throughput win (5-20x on a
rollout). max_new_tokens overrides this participant's per-turn cap for the batch (the co-stepper passes a
turn_cap here so a TokenBudget can shrink the final round). No tools/steering/capture/logprobs
here: callers needing those fall back to the per-conversation generate. Tokens are not guaranteed
identical to unbatched — batch composition and the single global RNG perturb rows (see PLAN §Execution
modes); only distributional reproducibility holds. metadata['batched'] marks these turns;
metadata['shared_prefill'] marks the fast path.
Source code in src/interlens/participant/participants/model_participant.py
parse_tool_calls
¶
Parse tool calls out of a generation. Base handles the common Hermes/Qwen <tool_call>{json}</tool_call>
format; families with other formats (Gemma's ```tool_code, Llama's <|python_tag|>) override
this. An unrecognized/absent call yields [] so the loop treats the output as a final message.
Source code in src/interlens/participant/participants/model_participant.py
render_tool_result
¶
Render a tool result as the standard structured tool message; the tokenizer's own template turns it
into the family-native format.
Source code in src/interlens/participant/participants/model_participant.py
split_reasoning
¶
Split a raw completion into (visible_content, parsed_think). Base handles the <think>...</think>
convention; families with other delimiters override this.
Source code in src/interlens/participant/participants/model_participant.py
QwenModelParticipant
dataclass
¶
QwenModelParticipant(
name: str = "",
hf_id: str | None = None,
weights_path: str | None = None,
dtype: str = "bfloat16",
attn: str = "flash_attention_2",
quant: str | None = None,
revision: str | None = None,
device: str | device | None = None,
max_new_tokens: int = 512,
temperature: float = 0.8,
top_p: float = 0.95,
seed: int | None = None,
thinking: bool | str = "auto",
system_prompt: str | None = None,
private_context: tuple = (),
tools: tuple = (),
max_tool_iters: int = 4,
kv_reuse: bool | str = "auto",
steering: object = None,
_model: "PreTrainedModel | None" = None,
_tokenizer: "PreTrainedTokenizerBase | None" = None,
)
Bases: ModelParticipant
A participant in a conversation that is a Qwen language model. Its tool-call format is the Hermes/base
<tool_call> JSON already handled by ModelParticipant, so this class adds no behavior — it exists so
Qwen models resolve to a distinct, statically-typed participant class.