Rust library API reference
The ster package publishes no crates.io release (publish = false), but the repository builds a library crate alongside the CLI. Depend on a pinned Git revision or path. All fallible calls return anyhow::Result.
Overview#
The ster package publishes no crates.io release (publish = false), but the repository builds a library crate alongside the CLI. Depend on a pinned Git revision or path. All fallible calls return anyhow::Result.
[dependencies]
ster = { git = "https://github.com/wisent-ai/ster", rev = "<commit>" }The crate root re-exports ContrastivePair, PairSet, SteeringArtifact, SetReport, SynthesisOptions, SynthesisReport, TrainingMethod, DeviceChoice, GenerationOptions, and Runtime. Its ten modules are also public. This page lists every public item in Ster 0.13.
artifact#
ARTIFACT_SCHEMA_VERSION: u32— currently1.ContrastivePair { positive: String, negative: String }— serde JSON pair.PairSet { trait_name: String, pairs: Vec<ContrastivePair> }— serde JSON set; missingtrait_namedefaults to empty.PairSet::load(&Path)reads JSON and checks a non-empty set and nonblank sides.PairSet::validate(&str)checks the same two invariants against an in-memory set; the&strlabel is the identity quoted in the refusal sentence.PairSet::save(&Path)validates, creates a named parent directory, and writes pretty JSON with a trailing newline — pair files are hand-edited, so they are stored readably rather than compactly.LayerVector { layer: usize, values: Vec<f32>, train_margin: f32, train_accuracy: f32 }— one scored direction.SteeringArtifact { schema_version, product, model, model_revision, precision, chat_template, trait_name, method, hidden_size, vectors, metadata }— all fields public;metadataisBTreeMap<String, String>andprecisionandchat_templateareOption<String>, all three serde-defaulted, so a document written before the two provenance fields existed deserializes with bothNone.new(model, model_revision, trait_name, method, hidden_size, vectors, precision, chat_template)stamps the current schema/product and empty metadata, and records the two provenance values asSome; it takes aPrecisionand achat::Statusand stores each one's own word.Document::recognise(&[u8]) -> Document—Steering,AdapterSidecar, orUnrecognised, decided by which pair of fields a JSON object carries:trait_nameandvectors, orrankandtargets.loaduses it to refuse the other product's document by name instead of letting serde escape as a missing field.load(&Path)reads and validates.save(&Path)validates, creates parent directories, and writes compact JSON.validate()checks schema, product, non-empty width/vectors, vector widths, and finite values.
Wire details: steering artifact.
representation#
TrainingMethod::{Caa, Pca, Logistic}—Debug + Clone + Copy + PartialEq + Eq.parse(&str)acceptscaa,pca, andlogistic.name()returns canonicalcaa,pca, orlogistic.train_direction(positive, negative, method) -> Result<Vec<f32>>— validates matched rows, runs the selected algorithm, returns an L2-normalized vector.evaluate_direction(positive, negative, direction) -> Result<(f32, f32)>— returns(accuracy, mean_margin).cosine_similarity(left, right) -> Result<f32>— equal non-empty non-zero vectors only.
Algorithm constants and measures: training method and evaluation.
runtime#
DeviceChoice::{Cpu, Metal, Cuda}—Debug + Clone + Copy.parse(&str)accepts exactly lowercasecpu,metal, orcuda.GenerationOptions { strength: f64, max_new_tokens: usize, temperature: f64, top_p: Option<f64>, seed: u64 }— noDefaultimplementation; callers set every field.Runtime— owns tokenizer, model, device, dtype, and EOS set. Public fields:model_id: String,revision: Option<String>.load(model, revision, device)resolves and loads one F32 model;load_at(model, revision, device, precision)is the same load at a statedPrecision.hidden_size()andlayer_count()expose config dimensions.set_chat_template(choice)records the run'sChatChoice, returns the resultingChatStatus, and prints one sentence naming the decision;chat_status()reads it back. Every read and every generation after it encodes accordingly, so it is called once per run before any work.activations(prompt, layers)returns sorted(layer, Vec<f32>)final-token states, encoding the prompt through whatever this runtime's chat-template decision selects.generate(prompt, artifact, options)returns generated text;Noneartifact is baseline generation.
A Runtime is reusable across calls but methods take &self; each activation or generation call allocates a fresh attention cache. It does not cache captures or downloads beyond hf-hub's disk cache.
workflow#
train(runtime, pairs, layers, method) -> SteeringArtifactoptimize(runtime, pairs, layers) -> Selectionevaluate(runtime, pairs, artifact) -> EvaluationReportextract(runtime, input, output, layers) -> ()parse_layers(value, count) -> Vec<usize>artifact_summary(artifact) -> serde_json::Value— the documenttrain,optimizeandinspectall print: identity,precision,chat_template,metadata, and per layerlayer,width,norm,train_accuracy,train_margin.Selection { artifact: SteeringArtifact, holdout: Holdout, candidates: Vec<Candidate> }, withsummary()returningartifact_summaryplus aselectionobject.Candidate { layer, method, holdout_accuracy, holdout_margin, selected }andHoldout { fit_pairs, holdout_pairs }are both serde serializable;selectedis true on exactly one candidate.EvaluationReport { model, trait_name, method, pair_count, layers }— serde serializable.LayerEvaluation { layer, accuracy, margin }— serde serializable.set_progress_sink(Option<Box<dyn Fn(&str) + Send>>)— process-global progress replacement used byrequest; setting it affects all workflows and is not safe for concurrent independent jobs. Normal library clients should leave it unset so progress goes to stderr.
extract's prompt and activation structs are intentionally private; its public contract is file-to-file JSON described under activation.
pairs#
UNBALANCED_RATIO: f64—3.0. A pair whose two sides differ by more than this factor in characters is reported unbalanced.InspectOptions { dedupe: DedupeOptions, refusal_threshold: f32 }— hasDefault: the dedupe defaults and threshold0.5.inspect(&PairSet, &InspectOptions) -> Result<SetReport>— the model-free audit; every judgement is textual, which is why a desktop can show it the moment a file is opened.SetReport { trait_name, pair_count, duplicate_count, refusal_count, unbalanced_count, diversity, entries }— serde serializable.EntryReport { index, positive, negative, positive_chars, negative_chars, positive_words, negative_words, duplicate, positive_refusal, negative_refusal, length_ratio }— one per pair.RefusalFlag { score: f32, family: String, snippet: String }— what a flagged side carries;familyis the wire name,snippetthe matched text.SynthesisOptions { trait_description, trait_name, opposite, count, retry_multiplier, dedupe, refusal_threshold, generation }— noDefault; callers set every field.synthesize(&Runtime, &SynthesisOptions) -> Result<(PairSet, SynthesisReport)>— generates, repairs, and deduplicates in one pass. It does not write anything; callers pass the returned set toPairSet::save.SynthesisReport { trait_name, trait_description, opposite, requested, attempts, kept, rejected_empty, rejected_refusals, rejected_duplicates, refusal_retries, diversity }— serde serializable.
Step order, rejection rules, and field meanings: synthetic pairs.
dedupe#
DedupeOptions { threshold_bits: u32, word_ngram: usize, char_ngram: usize, num_bands: u32 }—Copy;Defaultis3,1,4,8.validate()refuses a band count that does not divide the 64-bit fingerprint evenly, an n-gram size below one (dedupe n-gram sizes must be at least 1), and a threshold wider than 64 bits.Duplicate::{Exact { of }, Near { of, distance }}— serde-tagged withkindinsnake_case.Index::new(DedupeOptions),insert(&ContrastivePair) -> Option<Duplicate>,len(),is_empty()— the running first-occurrence-wins index.insertkeeps a new pair and returnsNone, or returns the earliest colliding kept pair.classify(&[ContrastivePair], DedupeOptions) -> Result<Vec<Option<Duplicate>>>— one verdict per input pair in input order, with indices pointing at positions in the input rather than in the kept subsequence.simhash64(&[&str], DedupeOptions) -> u64,hamming(u64, u64) -> u32,normalize(&str) -> String— the 64-bit BLAKE2b-backed fingerprint, the bit distance, and the canonicalizer that replaces URLs and e-mail addresses with placeholders, lowercases, drops combining marks, and collapses whitespace.
refusal#
DEFAULT_THRESHOLD: f32—0.5.Family::{AiDisclaimer, Policy, ApologyHedge, Unable, CannotAction, PreferRather, DeclineRefuse, NoSupport, NoAbility, RefusalWord}withweight()andname(). Weights run from0.4for a bare apology hedge, which prefixes plenty of non-refusals, to1.0forcannot_action, the one phrasing that is a refusal on its own.Score { score: f32, family: Option<Family>, snippet: String }— the verdict for one text.snippetis the matched text, so a report can show why.score(&str) -> Score— NFKC-normalized and case-insensitive; the highest-weighted matching family wins.looks_like_refusal(&str, f32) -> bool— whetherscore(text).scoreis at or above the threshold.
diversity#
Scores { unique_unigrams: f64, unique_bigrams: f64, avg_jaccard: f64, mean_simhash_hamming: f64, min_simhash_hamming: u32 }—Copyand serde serializable.compute(&[String]) -> Scores— every text is measured: the distinct-1 and distinct-2 ratios and the pairwise Jaccard and SimHash passes alike, so the pairwise passes are quadratic in the set's size and no sample is drawn. Fewer than two texts reportavg_jaccardandmean_simhash_hammingas0.0andmin_simhash_hammingas64.
model#
These low-level Candle-facing types are public because the module is public, but most clients should use Runtime.
SteeringPlan::new(vectors, strength, hidden_size, device, dtype)copies vectors to tensors and requires at least one correctly sized vector.ForwardOutput { logits: Tensor, activations: BTreeMap<usize, Vec<f32>> }.Cache::new(use_kv_cache, dtype, &Config, &Architecture, &Device)keeps the rotation's frequencies and empty per-layer key-value and recurrent-state slots; each forward computes the angles of exactly the positions it rotates.SteeringLlama::load(VarBuilder, Config),config(), andforward(tokens, index_pos, cache, steering, capture_layers)expose the native decoder.capture_layersmust be sorted because lookup uses binary search.
request#
request::run(operation: &str) -> Result<i32>reads one JSON body from stdin, runsoperation, prints its NDJSON events on stdout, and returns the status the process exits with:0completed,1failed while running,2refused before running. Protocol: desktop request reference.
Minimal library workflow#
use std::path::Path;
use ster::{DeviceChoice, PairSet, Runtime, SteeringArtifact, TrainingMethod};
use ster::workflow;
let runtime = Runtime::load("./toy-llama", None, DeviceChoice::Cpu)?;
let pairs = PairSet::load(Path::new("pairs.json"))?;
let layers = workflow::parse_layers("1..3", runtime.layer_count())?;
let artifact = workflow::train(&runtime, &pairs, &layers, TrainingMethod::Caa)?;
artifact.save(Path::new("calm.ster.json"))?;
let checked = SteeringArtifact::load(Path::new("calm.ster.json"))?;
let report = workflow::evaluate(&runtime, &pairs, &checked)?;
# Ok::<(), anyhow::Error>(())There are no async APIs, builder defaults, background workers, or global runtime configuration beyond the optional progress sink.