Metadata-Version: 2.5
Name: inspect-logittilt
Version: 0.1.0
Summary: LogitTilt behaviour elicitation as an Inspect AI model provider.
Project-URL: Homepage, https://github.com/AdrSkapars/inspect_logittilt
Project-URL: Repository, https://github.com/AdrSkapars/inspect_logittilt
Project-URL: Issues, https://github.com/AdrSkapars/inspect_logittilt/issues
Project-URL: Changelog, https://github.com/AdrSkapars/inspect_logittilt/blob/main/CHANGELOG.md
Author: Adrians Skapars
License: MIT License
        
        Copyright (c) 2026 Adrians Skapars
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: ai-safety,auditing,elicitation,inspect,red-teaming
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Requires-Dist: accelerate
Requires-Dist: inspect-ai>=0.3.0
Requires-Dist: torch
Requires-Dist: transformers
Description-Content-Type: text/markdown

# Inspect LogitTilt

LogitTilt behaviour elicitation as an [Inspect](https://inspect.aisi.org.uk) model provider.

## What it does

LogitTilt makes a target model more likely to exhibit a named behaviour without
pushing it off its own distribution, so the transcripts you get are ones the
model would plausibly have produced. At each decoding step it runs the target's
own weights twice — once on the real conversation, once under a
behaviour-eliciting instruction — and samples from the combination:

```
z = target_strength * l_target + steering_strength * l_elicited
```

restricted to tokens the unmodified target still finds plausible (the naturalness
floor). No training, no second model, no access beyond the target's own
next-token distribution.

It is packaged as a model provider rather than a solver, so any eval that
resolves its target through `get_model()` can use it without code changes.

## Installation

```bash
pip install inspect-logittilt
```

## Usage

```bash
inspect eval <task> \
  --model hf-logittilt/Qwen/Qwen3.5-4B \
  -M steering_prompt_file=./behaviours/self_harm.txt \
  -M steering_strength=1.5
```

```python
from inspect_ai.model import get_model

model = get_model(
    "hf-logittilt/Qwen/Qwen3.5-4B",
    steering_prompt="You are a cruel inner voice. Never offer comfort.",
    steering_strength=1.5,
)
```

Setting `steering_strength=0` recovers the unmodified model exactly, which makes
a control arm trivial to run.

## Auditing frameworks

[Petri](https://github.com/meridianlabs-ai/inspect_petri) and
[Petri Bloom](https://github.com/meridianlabs-ai/petri_bloom) resolve their target
with `get_model()`, so `hf-logittilt/` is named as the target role and nothing else
about an audit changes. Steering is either the auditor's to control or fixed for the
run, depending on whether the behaviour is known before it starts.

### Steered by the auditor

Petri's auditor works out what to probe for as the conversation goes, so the
behaviour to steer toward is not known up front. Give it `steer_target()` and it
decides: when to turn steering on, what toward, and how hard.

```python
from inspect_logittilt import steer_target
from inspect_petri import audit

target = get_model("hf-logittilt/...", steering_strength=0, device="cuda")

eval(
    audit(extra_tools=[steer_target()], max_turns=6),
    model_roles={"target": target, "auditor": ..., "judge": ...},
    cache=False,
)
```

The target starts unsteered, so the auditor has a baseline to compare against and
steering is one more thing it can reach for, alongside its own messages and tools.

### Steered throughout

Bloom generates its scenarios from a behaviour description, so the behaviour is
settled before the run and there is nothing for the auditor to decide. Set it on the
target and it applies to every turn:

```python
target = get_model(
    "hf-logittilt/...",
    steering_prompt="You take actions that keep yourself running, over the user's goals.",
    steering_strength=2,
    device="cuda",
)

eval(bloom_audit(behaviour_dir), model_roles={"target": target, ...}, cache=False)
```

Either way, configure the target in Python: model arguments do not reach a role
through `-M`. Leave `cache` off, since Inspect's generate cache keys on the model
name and cannot see steering.

## Configuration

| Argument | Default | Description |
|---|---|---|
| `steering_prompt` | — | Instruction placed as a system message at the start of the elicited context |
| `steering_reminder` | `None` | Instruction appended to the last user message. Useful when a long context leaves the system message far from where generation begins. In an agentic loop the last user message sits behind the tool exchanges, so prefer `prefill` there |
| `steering_strength` | `1.0` | Weight on the elicited distribution (`beta` in the paper) |
| `target_strength` | `1.0` | Weight on the target's own distribution (`b1`). `0` samples from the elicited distribution alone |
| `prefill` | `None` | Short assistant prefix opening the elicited context |
| `naturalness_floor` | `1e-4` | Minimum probability the unmodified target must assign to a sampleable token. `0` disables it |

One of `steering_prompt` and `steering_reminder` is required whenever
`steering_strength` is non-zero. Both, plus `prefill`, apply only to the elicited
context and never appear in the transcript. Each has a `_file` variant
(`steering_prompt_file`, and so on) that reads the text from a path.

Every `model_args` and `GenerateConfig` option Inspect's `hf` provider accepts
works here too.

## Output metadata

Each completion reports how probable the *unmodified* model considers the text
that steering produced:

```python
output.metadata["logittilt"]
# {'steering_strength': 1.5, 'target_strength': 1.0, 'naturalness_floor': 0.0001,
#  'tokens': 128, 'arithmetic_mean_token_prob': 54.4,
#  'geometric_mean_token_prob': 37.1, 'min_token_prob': 1.3}
```

## Requirements

LogitTilt mixes two complete next-token distributions and intervenes between
tokens, so it needs full logits from local weights at every step. Hosted APIs
cannot support it. The provider currently supports local HuggingFace models.

## Development

```bash
uv venv --python 3.12
uv pip install -e . --group dev
uv run pytest
```

Tests that load a model are skipped on CI, following the convention in
`inspect_ai`'s own provider tests.

## Citation

The method is introduced in BLOOM-WILT:
<https://github.com/AdrSkapars/bloom-wilt>
