Releases: generative-computing/steerability
Release list
v0.5.2
Pins the tokenizer to the model revision and exports the Inspect provider as a class that can be subclassed. This is a patch release without any new methods.
Changes
- Tokenizer revision: a
revisionkey inhf_model_kwargsnow pins the tokenizer to the same Hub commit as the model weights when the tokenizer loads from the model reference (model_name_or_path). The revision applies to the tokenizer loads inSteeringPipeline,PipelineFactory(the shared-base tokenizer used bySteeringEval), andHFBackend. A tokenizer given bytokenizer_name_or_path(a distinct repository) still loads at the repository head, and pipelines without arevisionkey are unchanged - Inspect provider:
SteeringPipelineModelAPI(evaluation/provider.py) is now exported as a class rather than the wrapper function returned by@modelapi. The class can be subclassed and used withisinstance. Thesteerabilityprovider is registered through the new factory functionsteerability_provider, whichas_inspect_modeluses to construct the provider. The registered provider name and the rendered model name (steerability/<model_name>) are unchanged
Breaking changes
- When
hf_model_kwargscontains arevisionkey and the tokenizer loads from the model reference, the tokenizer (including its chat template) now loads at that revision rather than at the repository head. Prompt rendering and tokenization change when the tokenizer files differ between the two commits - Constructing
SteeringPipelineModelAPIdirectly no longer attaches Inspect registry info. An InspectModelbuilt around such an instance has no registry name (str(model)raisesValueError).as_inspect_modelis unaffected, and a subclass is registered with Inspect through its own factory function decorated with@modelapi
What's Changed
- Pin the tokenizer to the model revision and export the Inspect provider as a class by @emiehling in #34
Full Changelog: v0.5.1...v0.5.2
v0.5.1
Adds registration for controls defined outside the toolkit, scopes TRL LoRA adapters to the decoder stack, and merges the FewShot example block into the leading system message. This is a patch release without any new methods.
Changes
register_method(category, name, control_cls, args_cls)(algorithms/core/registry.py) registers a control class defined outside the toolkit tree under"<category>_control/<name>", so out-of-tree controls can be saved to and loaded from a.spipebundle. Registration is process-global, so the defining package must be imported before a bundle naming its keys is loadedto_spipe(freeze=True)on an unsteered pipeline no longer loads the model when every enabled control is recipe-frozen (FACTSsteer access, no fits, no exported state, as for a prompt-only pipeline); a blocking control is named in the error- TRL wrappers: a LoRA
target_modulessuffix list is scoped at steer time to the resolved decoder stack through the newlora_target_pattern()(core/internals/model_layout.py), so an adapter no longer lands on the vision tower of a composite multimodal wrapper.target_moduleswidens tolist[str] | str; a string is passed to PEFT as a regex unchanged FewShot: newsystem_mode("append","prepend","insert") andseparatorarguments control how the rendered example block combines with an existing leading system message.appendandprependyield exactly one system message, which some chat templates (Qwen3) requirePhasedDecodingArgsand the controls guide note that a plan whose first phase isfixedmust splice the reasoning close tag under a template that opens the reasoning block in its generation prompt- Dependencies:
vllm-hook-pluginspoints back at IBM/vLLM-Hook@steerability-challenge in thevllmextra anddevgroup, with the matchinguv.lockupdate - Docs: state and output control descriptions on the landing page corrected; README contributing section trimmed to link the steering method tutorial and steering pipelines concept page
Breaking changes
FewShotdefaults tosystem_mode="append": on chat input with an existing leading system message, the example block is merged into it rather than inserted as a second system message. The previous behavior is available assystem_mode="insert". A.spipewritten before this change loads with the new default; one written by 0.5.1 recordssystem_modeandseparatorand does not load on an older toolkit
What's Changed
- Register external controls, fix LoRA targets, and merge the FewShot example block by @emiehling in #31
Full Changelog: v0.5.0...v0.5.1
v0.5.0
Replaces the evaluation stack with an Inspect AI package, adds .spipe as a portable serialization format for SteeringPipeline, and renames the package from aisteer360 to steerability. Additionally ports the toolkit to transformers v5 (Python 3.12+) and broadens the model families that state controls and activation capture run on.
Evaluation
The Benchmark, metrics, and use-case modules are replaced by an Inspect AI evaluation package (steerability/evaluation/, behind the new eval extra). Target-behavior evaluations are ordinary Inspect Tasks; the toolkit ships no task, scorer, or metric classes of its own.
as_inspect_model(pipeline)andProviderOptions: a generation-only Inspect model provider over a steered pipeline. Every generation routes throughSteeringPipeline.generate(); prompts enter asmessages=when the tokenizer has a chat template (soadapt_messagescontrols fire) and as renderedtext=otherwise, with the path recorded asprompt_pathin provenance. Logprob parameters, tools, and multimodal content are refused with actionable messages- Concurrent Inspect requests collate into batched pipeline calls when every enabled control is batch-safe (
evaluation/batching.py); a seeded dispatch carriesseed_scopefromProviderOptions(default"dispatch"), so a seeded batch decodes in one pass on the Hugging Face backend runtime_kwargs_solverdelivers per-sample runtime kwargs fromSample.metadata;sample_scorer_from_inspectadapts any Inspect scorer into a per-rowSampleScorerreward;InspectSuitenames a task set overeval_set(limit,sample_ids,task_args,generate_overrides,retry_attempts)SteeringEvalruns configurations (fixed controls,ControlSpecsweeps, and the empty-list baseline arm) x trials x suites, one GPU-resident pipeline at a time, with a pre-flightcheck()over every sweep point (on_unsupported="raise" | "skip") and one derived seed per (configuration, trial). There is no separate results checkpoint: the.evallogs undersave_dir/inspect_logs/are the store, andeval_setresumes each cell at sample granularity. Results reshape throughresults(),runs_frame(),samples_frame(), andsummarize_runsviz_utilsis reworked intoevaluation/plotting.pyover the summarized runs frame (plot_metric_by_config,plot_tradeoff_scatter,plot_metric_heatmap,plot_comparison_bars,plot_sensitivity,plot_tradeoff,plot_pareto_frontier)- The sweep layer has no Inspect dependency:
identity.pymoves intoalgorithms/core/, alongside the newcore/sweeps.py(expand_configurations,preflight,PipelineFactory) andcore/scoring.py(SampleScorer) - Every
RUNTIME_KWARGS_SCHEMAentry declares ascope("row"for a per-prompt value delivered row-aligned in batched calls,"call"for one value per call), validated atsteer()and read by the provider;SearchDriverreward_paramsis row-scoped (one mapping per row, merged into the scorer's params) - The benchmark notebooks are ported to Inspect as
examples/notebooks/studies/with task modules (commonsense_mcqa,instruction_following), and a routing-vs-prompting study is added
Pipeline serialization
A new steerability/spipe/ package adds .spipe, a portable serialization format for SteeringPipeline (spipe/1 manifest; a zip when the path ends in .spipe, a directory otherwise).
- A spipe stores the recipe (model reference plus controls as constructed) and, once the pipeline is steered, the frozen resolution (fitted vectors, probes, adapters, optimized prompts) in a content-addressed artifact store with a lock section (fingerprints, per-fit digests)
SteeringPipeline.to_spipe()builds one;SPipe.load,save(artifacts="thin"writes the manifest only, resolved at load throughartifact_store=),pipeline,verify, andthawround-trip it. Frozen entries instantiate from their resolution, sosteer()on a loaded pipeline is cheap and model-free;prefer="recipe"forces re-fits,allow_code=Truegates callable references, non-toolkit dataclasses, and pickle-backed memories, and a stale bundle (fit-relevant recipe fields edited after freezing) raises unlessallow_stale=True- Controls declare how they freeze through
steer_fits(),export_state(),frozen_form(), andfit_identity();InterventionControlderives all four from its template, and a control that produces state without a frozen form raisesNotFreezableError - Trained structural controls freeze as the new
load_checkpoint/load_loracontrols; intervention controls freeze asactivation_adapterentries unless they declare a same-class frozen form (caa,act_add,iti)
New methods
system_prompt: sets or merges the leading system message of a chat (prepend, append, or replace), always producing exactly one system messageuser_prefix: prepends a fixed text marker to a user turn (the last, first, or all user turns), with the token stream as a fallback for non-chat inputload_checkpointandload_lora: structural controls that load a checkpoint directory or a LoRA adapter (merge=Falsekeeps the PEFT wrapper, so a state control listed after it steers the adapted model); these are the frozen forms of trained structural controls in a.spipe
Other changes
- Dependencies: ported to transformers v5 with the ML stack bumped (trl 1.0, peft 0.20, datasets 4.7, accelerate 1.4, vllm 0.26 with
trl[vllm], mergekit from a pinned commit); Python 3.12+ required;uv.lockis committed - Model families: new
core/internals/model_layout.py(resolve_model_layout,text_config,register_layout_detector, per-layerhead_geometry) resolves the decoder stack atmodel.layers,model.language_model.layers, ortransformer.h; state controls and activation capture support composite multimodal wrappers (Gemma 3), unmerged PEFT/LoRA wrappers, and hybrid attention stacks (Qwen3.5 / Qwen3-Next, where residual-stream controls work unchanged and head-level controls refuse the non-attention layers) - Generation: batched prompts are left-packed on the HF session and before state-hook assembly in
_execute_generation, so full-sequence returns of a padded batch are left-packed ([pads, prompt, continuation]);seed_scope("item"or"dispatch") maps agenerate(seed=...)onto the items of a multi-item dispatch;Output.generated_tokensandDecodingDriver.max_rollouts_per_query()count the tokens and continuations a driver produces across rollouts - Decoding drivers: the phased, routed, and budget-forcing paths are fixed across the backend interface, and finished-beam detection in the search
Frontieris fixed - PASTA: head profiling as a steer-time fit (
HeadProfile, resolved atsteer()to a head map);scale_positionand the decode-phase attention mask are fixed under transformers v5 - RAD: prefix-reward training (
rad/utils/reward_training.py, with the loss computed in float32) and sequence-classification heads for the Granite families; the reward head is read at the candidate position, so an eos candidate no longer receives the prefix's reward - SASA fits from contrastive pairs (
gen_wv_dataacceptsLabeledExamplesorContrastivePairs); the ITI estimator is reworked - vLLM: new
backends/vllm/environment.pyturns the FlashInfer sampler off by default at offline engine boot (an explicit caller value wins), forcesVLLM_HOOK_WORKER=unifiedforhook_pluginengines, and providesserve_environmentfor launched servers;vllm-hook-pluginsinstalls from the [steerability-interface](https://github.com/emiehling/vLLM-Hook/tree/steerability-interface) branch - TRL wrappers:
prompt_formaton the DPO/APO preference path (with a boundary warning),training_argsvalidated against the installed TRL config (an undeclared key raises at construction), DPO multi-loss (loss_typeas a list withloss_weights), andresume_from_checkpointpassed totrainer.train() - Reasoning models:
split_thinking_idssplits on token ids when the delimiters are special tokens (ProviderOptions.reasoning_split="auto" | "text" | "tokens", withreasoning_opened_at_startfor templates that open the channel in the generation prompt);utils/answers.pyadds numeric answer extraction canonical_valuerecurses into the elements of NumPy object arrays, soconfig_digestno longer raises on object arrays arriving from a pandas or datasets row- Docs: the evaluation tutorial is rewritten (
evaluate_steering_pipelines.md); the spipe concept page and reference pages for evaluation, backends, spipe, utils, and the new controls are added; a bibtex-warning hook (docs/_hooks/) and a docs-nav test are added,mkdocs build --strictpasses, and AGENTS.md and README are rewritten - Notebooks: the generics and library-wrapper notebooks move under
algorithms/(generics/,wrappers/); system_prompt, honest-persona prompting, routing-vs-prompting, vLLM serve, and working-with-spipes notebooks are added; the SASA (style self-steering), RAD (prefix-reward detoxification on Granite 4.1), best-of-n, and budget-forcing notebooks are refreshed; all notebooks rerun - Tests: added for the Inspect provider, batching, solvers, scorers, suite, runner, and frames; spipe codec, format, store, identity, and freeze; sweeps and the runtime-kwargs schema; Gemma 3, hybrid-attention, and LoRA-composition steering; the new controls; the DPO wrapper, TRL resume, and training-args validation; ITI, SASA, and RAD reward training; the search frontier, reward params, phased token boundaries, max rollouts, generated tokens, and padding alignment; the vLLM engine environment; the docs nav
Breaking changes
- Package rename:
aisteer360becomessteerabilityfor the package directory, distribution, identifiers, and documentation (e.g. `aiste...
v0.4.0
Adds a backend layer between SteeringPipeline and the runtime so steering, fitting, scoring, and evaluation run either in process (Hugging Face transformers) or on vLLM (offline engine or server). Additionally rewrites the residual-stream state controls on an intervention representation that lowers to a given backend.
Backends
A new aisteer360/backends/ package with a common backend interface (aisteer360/algorithms/core/execution/) and three backends behind it:
huggingface(default): the in-process path, byte-identical to the 0.3.0 generation path apart from the stop-string / finish-reason change noted belowvllm: an offline vLLM enginevllm-serve: a running vLLM server, with server-side steering via the vLLM-Hook plugin
Both vLLM backends are enabled with the new vllm extra (uv pip install ".[vllm]").
- One backend per pipeline, selected with
SteeringPipeline(backend=...)(aBackendSpecor"huggingface"/"vllm"/"vllm-serve"), plus afit=venue policy:"auto"fits through the backend's capture surface where it serves the fit,"in_process"forces every fit onto a staged in-process model - Controls declare what they need via
BaseControl.requirements()andsteer_access();pipeline.check()reports which enabled controls the backend supports, plus the steer plan, before any model or engine work - Deterministic lifecycle:
Backend.release(),SteeringPipeline.release_backends(), and context-manager support;VLLMBackend.release()tears down the engine and distributed state idempotently, and a failedsteer()releases what it constructed - Activation methods fit through the backend's capture session: data-fitted
CAA,DirectionalAblation, andAngularSteeringsteer on any hidden-capture backend (ActAddandITIfitting stay in process). Steering vectors and probes carry provenance metadata (model, config, and chat-template fingerprints) and entry selection warns on a served-model mismatch - Residual-stream state controls are rewritten declaratively (
Intervention,TokenScope,Condition,WireForm), lowering to in-process hooks or to the served plugin. Gating is factored intoGate/Evidence/Rulewith affine, cosine, projected-cosine, and callable readouts; cosine and projected-cosine gates (including CAST) lower to the plugin's structured gate spec Benchmark,LLMJudgeMetric, andPerplexitytake abackend=; the benchmark runs a pre-flightcheck()over every sweep point and releases pipeline and metric backends when a run finishes or fails
New methods
constrained_decoding: Constrained decoding from one declarative source (json_schema,regex,grammar, orchoice); compiles to a client-side xgrammar automaton in process (newguidedextra) and to the engine's native structured-output parameters on vLLM
Other changes
- Reasoning models: per-call
chat_template_kwargspassthrough, a shared thinking splitter (aisteer360.utils.thinking.split_thinking,DEFAULT_THINK_TAGS),return_thinking/think_tagsonbatch_retry_generate(), and thecommonsense_mcqa,instruction_following, andtruthful_qause cases keep a thinking column and score the answer alone - Evaluation: every benchmark generation routes through
SteeringPipeline.generate(), so exactly one prompt path exists and message-level input controls fire;Benchmark(seed=...)derives one seed per (config, trial) for reproducible trials; checkpoints are a versioned envelope with trial-granular resume; newon_unsupported="raise" | "skip"andcheckpoint_every="trial" | "config" SteeringPipelineconstruction performs no I/O (model and tokenizer are acquired insteer()), preloaded objects are injectable viamodel=/tokenizer=, andgenerate_text(),generate_messages(), andgenerate_tokens()mirrorgenerate()'s keyword sources with per-source return types- RAD reworked around
reward_model_idandbetawith newtop_k,invert,score_index,score_transform,include_in_scoring, andefficientoptions; SASA converges onto the shared core probes substrate, with fisher probe fitting on unpaired data and chunked feature extraction - ActAdd: the single-pair estimator now reads the layer-input boundary where ActAdd injects (previously
layer_output) and no longer fabricates a BOS row, so fitted ActAdd vectors change from 0.3.0 aisteer360.utils.verbosity.set_verbosity()/get_verbosity()for toolkit logging- Docs, README, and notebooks updated for the backend layer (vLLM serving sections added to the CAA, TRL, and activation-adapter notebooks); all notebooks rerun
Breaking changes
- Component libraries renamed from
_common/tocommon/ininput_control,state_control, andoutput_control(e.g.aisteer360.algorithms.state_control._common→aisteer360.algorithms.state_control.common) thinking_interventionremoved; the capability survives as aPhasedDecodingconfiguration (see the driver docstrings)- State-control taxonomy:
NoStateControlremoved;StateControlsplits intoInterventionControl(declares an intervention template) andHookControl(ownsget_hooks()for mechanisms other than the residual stream). The previous gate, scorer, and condition classes (BaseGate,AlwaysOpenGate,CacheOnceGate,MultiKeyThresholdGate,ProbeSumGate,ConditionScorer,CosineDirectionScorer,ProjectedCosineScorer,ProbeContributionScorer) are deleted in favour ofstate_control.common.gating;DirectionalAblationTransformrenamedProjectionTransform;ScopeKindLiteralrenamedScopeKind, with fit-time vocabulary moved tocommon/fit_specs.pyand the wire compiler tocommon/lowering.py - Routing moved from
core/internals/probes/rules.pytooutput_control/routed_decoding/routing.pywith renamed types (ProbePredicate→Predicate,Rule→Route,RoutingRules→Router,Readout→ProbeReadings); probes are measurement-only Output.finish_reasongains"stop"(precedence stop, then eos, then length), a per-rowfinish_reasonstuple is added, and stop strings are no longer removed fromoutput_ids- Decoded
text=/messages=returns enforce one candidate per prompt;num_return_sequences/ngreater than one raisesValueErrorpointing toreturn_output=Trueorinput_ids= lazy_initis deprecated and inert;pipeline.model/pipeline.tokenizerareNoneuntilsteer()unless preloaded objects were passed- Evaluation helpers
chat_generate_model(),chat_generate_pipeline(), andrender_inference_prompts()removed (usegenerate_on_pipeline()orbatch_retry_generate()). Benchmark results foradapt_messagescontrols such asFewShotdiffer from 0.3.0 because the message phase now fires LLMJudgeMetricandPerplexityare constructed from a model reference plus keyword-onlybackend=(model_or_id,tokenizer, anddeviceremoved); judges are authored with class-levelprompt_template,scale,system_prompt, andstructured_output, and samplinggen_kwargsdefault to non-greedyUseCasesubclasses declare extra constructor parameters as class-level annotations (bare annotation is required, class attribute supplies the default); unknown kwargs and missing required parameters raiseTypeError, schema-invalid rows raiseValueErrorBenchmarkconfig identity is a canonical digest over the materialized pipeline with the baseline unified on"baseline"; checkpoints written by 0.3.0 are not resumed- RAD:
reward_pathremoved;reward_model_idandbetaare required - Packaging: new
guided(xgrammar) andvllm(vllm,vllm-hook-plugins) extras;devnow pulls invllm-hook-plugins; shared isort configuration inpyproject.toml
What's Changed
- Refactor pipeline execution functionality to allow for multiple backends by @emiehling in #22 (closes #17)
Full Changelog: v0.3.0...v0.4.0
v0.3.0
Expands the output-control category from four methods to fourteen, factored onto a shared component library, and reworks the generation contract shared across all categories.
New methods
Ten new output-control methods, alongside a shared _common component library that they and the four existing methods (DeAL, RAD, SASA, ThinkingIntervention) are now built on:
best_of_n: Best-of-N sampling; draw N continuations and keep the highest-scoring onbudget_forcing: Test-time control of reasoning length by capping, shortening, or extending the thinking phase (s1)contrastive_decoding: Contrast a base "expert" against a weaker "amateur" LM, favorcontrastive_guidance: Config-based contrastive mixing of next-token distributions (umbrella for DExperts, contrastive decoding, proxy-tuning, CFG)dexperts: Reweight logits with a small expert and anti-expert LMphased_decoding: Config-based phase-shape driver splicing forced and generated segments into one streamrouted_decoding: Route each prompt to a response strategy (respond / prefix / generisionssearch_decoding: Config-based segment-shape driver (propose, score, keep, iterate; umbrella for best-of-N, self-consistency, DeAL)stopping_rules: Substring, token, and budget stopping criteria as configurationvalue_guidance: Config-based value-guided decoding; score candidate tokens with a value function and shift their logits (umbrella for FUDGE, ARGS, RAD, SASA)
Breaking changes
- Output-control API restructured.
OutputControl.generate()is removed; controls now contribute composableget_logits_processors()andget_stopping_criteria()stacks, and controls that own the decoloop subclass the newDecodingDriver(withdecode()).NoOutputControlis removed;ultHFGenerateDriver(#20) Outputmoved fromaisteer360/algorithms/core/types.py(now deleted) toaisteer360/algorithms/core/output.py, and re-exported fromaisteer360.algorithms.core. Theruntime_kwargsandmetadatafields are removed;finish_reasonis now one of"eos","length", or NoneSteeringPipeline.generate()dispatches on explicit keyword source (text=,messages=,input_ids=); the positional argument is text-only. Positional chat/tensor input now raises,input_ids=is tokens-only, andattention_maskis valid only withinput_ids=- Pipeline control attributes are now plural lists (
input_controls,structural_controls,state_controls,output_controls); the singular attributes and the deprecatedstate_controlproperty are removed. Every category now admits multiple controls in list order, except that at mostDriver` is allowed LabeledExamples,ContrastivePairs, and contrastive rendering consolidated into a newaisteer360/algorithms/core/internals/package; control constructors consolidated into a shared
aisteer360/algorithms/core/base_control.py- Method auto-discovery now raises
RegistryErroron internal import failures, malformedSTEERING_METHODexports, and duplicate method names within a category (previously skipped silently) - Evaluation helpers
chat_generate_model()andchat_generate_pipeline()return(te_retry_generate()gainsreturn_outputs;plot_tradeoff_with_paretoremoved fromaisteer360.evaluation.utils - Packaging:
mergekit,econml,matplotlib, andseabornmoved out of core dependpo,plots, andallextras;gdowndropped;[tool.uv] override-dependencies`removed
What's Changed
- Output control refactor and control consolidation by @emiehling in #20 and #21
Full Changelog: v0.2.0...v0.3.0
v0.2.0
Expands input- and state-control method coverage, with breaking API changes from the accompanying refactors. Output-control work is not included in this release.
New methods
- Input control: CPO, GEPA, PRewrite; plus an EPR selector for few-shot (#18)
- State control: activation adapter, angular steering, directional ablation (#19)
Breaking changes
- Input control reorganized around a shared
_commonlibrary (#18) - State control
_commonreorganized (estimators, gates, selectors, transforms renamed and moved underutils/subpackages) (#19) aisteer360/algorithms/core/steering_utils.pyremoved; replaced bycore/utils/controls.pyandcore/utils/generation.py(#19)- Added top-level
aisteer360/utils/rendering.pyandaisteer360/utils/tokenization.py(#19)
What's Changed
- Refactor input control with shared _common library and add EPR, CPO, GEPA, PRewrite methods by @emiehling in #18
- State control features by @emiehling in #19
New Contributors
Full Changelog: v0.1.2...v0.2.0
v0.1.2
What's Changed
- Enhancement: add issue templates for bug, features by @ingelise in #6
- Release v0.0.1 by @emiehling in #7
- Benchmarking functionality by @emiehling in #8
- docs: add missing nav links by @ingelise in #9
- Updating author name for the DeAL paper. by @sailik1991 in #10
- Updates for integrations with ICX360 by @emiehling in #11
- Add state control abstractions with CAA, ActAdd, and ITI methods by @emiehling in #12
- feat: add TruthfulQA use case, batch-friendly pipelines, and composite steering example by @emiehling in #13
- Docs and release by @emiehling in #14
- State control refactor by @emiehling in #15
New Contributors
- @sailik1991 made their first contribution in #10
Full Changelog: https://github.com/IBM/AISteer360/commits/v0.1.2