Inspect AI Converter
Inspect AI is the primary upstream the format was designed around — the converter preserves messages, scores, generation config, eval / run ids, per-token logprobs, the model’s response region as a generated annotation, and any reasoning / tool-call sections detected inside the chat template.
from inif.converters.inspect_ai import from_eval_log, from_eval_fileRequires the inspect extra:
pip install "inif[inspect]"from_eval_file
The thin wrapper for the file-based case:
from inif.converters.inspect_ai import from_eval_file
doc = from_eval_file("logs/run.eval")It calls inspect_ai.log.read_eval_log(path) then forwards to from_eval_log. The source path is appended to metadata.sources.
from_eval_log
The full-fat entry point. Pass an inspect_ai.log.EvalLog you already have in memory:
from inspect_ai.log import read_eval_log
from inif.converters.inspect_ai import from_eval_log
eval_log = read_eval_log("logs/run.eval")
doc = from_eval_log(eval_log, tokenizer="meta-llama/Llama-3.1-8B")| Arg | Default | Effect |
|---|---|---|
tokenizer |
"auto" |
See Tokenizer resolution. |
include_messages |
True |
Include per-message text segments on Sample.texts. |
deduplicate |
True |
Run sequence dedup. |
min_sequence_length |
5 |
Minimum n-gram length for dedup. |
tag_chat_roles |
True |
Annotate system / user / assistant / tool / template. |
tag_generated |
True |
Tag the model’s response with "generated" (last assistant message). |
tag_reasoning |
True |
Annotate reasoning and tool-call renderings ("reasoning", "tool_call"). |
extract_logprobs |
True |
Attach per-token logprobs when the eval log carries them. |
What gets preserved
Per-sample fields
| Sample field | Inspect AI source |
|---|---|
id |
inspect_sample.id (coerced to str). |
texts |
One Text per message extracted from inspect_sample.messages, with token offsets and per-section children for assistant turns (reasoning / content / tool_calls). |
tokens |
tokenizer.apply_chat_template(messages, tokenize=True). |
scores |
inspect_sample.scores — one SampleScore per scorer. |
target |
str(inspect_sample.target) if present. |
input_tokens, output_tokens |
inspect_sample.usage.input_tokens / .output_tokens. |
Document metadata
| Metadata field | Inspect AI source |
|---|---|
model.name |
eval_log.eval.model. |
model.revision |
Tokenizer’s _commit_hash when available. |
model.generation_config |
Merged from eval_log.plan.config and eval_log.eval.model_generate_config — captures every non-None GenerateConfig field (sampling: max_tokens / temperature / top_p / top_k / seed / stop_seqs / frequency_penalty / presence_penalty / best_of / logit_bias; reasoning: reasoning_effort / reasoning_tokens / reasoning_summary / reasoning_history; tool use: parallel_tool_calls / max_tool_output; transport: max_connections / max_retries / timeout; plus extra_headers / extra_body / response_schema / modalities / …). Always includes preserve_reasoning: True (see callout below). |
source_eval.extra |
Eval-time run options: eval_config (sample / message / token / time / cost limits, epochs, error-handling), plus task_args, solver, solver_args, model_args when set. |
source_eval |
framework="inspect_ai", plus framework_version, task, task_version, eval_id, run_id. |
total_time |
(eval_log.stats.completed_at - started_at).total_seconds(). |
packages |
inif, transformers, inspect_ai versions. |
Annotations
With the defaults, every sample gets:
- Chat role annotations (
tag_chat_roles=True):user,assistant,system,tool, andtemplateranges via character-span matching against the formatted chat template. Best-effort: silently skipped when a tokenizer’s per-token decode doesn’t round-trip. generatedannotation (tag_generated=True): covers the last assistant message — the model’s actual response — withmetadata={"source": "converter"}.reasoning/tool_callannotations (tag_reasoning=True): for every assistant message that emitted reasoning or tool calls, the chars the chat template rendered for those sections are detected via a render-with-vs-without diff and tagged. Tokens in the diff window pick up both the field’s annotation (reasoning/tool_call) and the message’s role (assistant), moving out oftemplateiftag_chat_roleshad labelled them there. Fully model-agnostic — the chat template itself decides where each field renders.
The generated annotation is added before sequence deduplication so the response region never collapses into a shared sequence (annotations block dedup). Reasoning / tool_call are added after dedup, since they only need to reflect the final tokenization.
Per-token logprobs
When the eval log includes choice logprobs (output.choices[0].logprobs.content) and extract_logprobs=True, the per-token logprobs are attached to the response tokens via set_extra("logprob", lp). The viewer renders them as hover tooltips and underline indicators.
This is best-effort: when the eval-source tokenization disagrees with the configured tokenizer on token count, the logprobs are silently skipped rather than mis-aligned. To force a match, pass the tokenizer that produced the original logprobs.
Decoding strategy
The converter picks one decode path per archive (not per sample) based on what the tokenizer supports:
- Offset mapping — fast tokenizers exposing
return_offsets_mapping. Per-token strings are exact slices of the formatted template. Multi-byte UTF-8 characters split across byte-level tokens (e.g.♠U+2660 → 2 tokens on Qwen3.5) report identical(start, end)offsets on every fragment; the decoder trackslast_consumedso the character is emitted by the first fragment and subsequent fragments yield"". A final"".join(pieces) == formattedintegrity check returnsNoneon any remaining mismatch so the converter falls back to the next tier. - Byte-level slicing — byte-level BPE tokenizers (e.g. Kimi’s tiktoken variant) without offset support. Rebuilds per-token bytes via
tokenizer.byte_decoder+encoder. - Per-token decode with cache — fallback for tokenizers with neither of the above. May produce U+FFFD for multi-byte characters split across BPE tokens; chat-role tagging silently skips affected samples.
The choice is probed once on the first sample and reused, so the cost is amortized across the whole conversion.
Common patterns
Convert one file
from inif.converters.inspect_ai import from_eval_file
doc = from_eval_file("logs/run.eval")
doc.save("run.inif.json")Override the tokenizer
doc = from_eval_file("logs/run.eval", tokenizer="meta-llama/Llama-3.1-8B")A UserWarning is emitted if the tokenizer disagrees with the eval log’s model id. The warning is informational only — the conversion proceeds.
Batch from the CLI
inif convert eval logs/*.evalEach input writes to its own output file in the same directory.
Filter to failing samples after conversion
failures = doc.filter_samples_by_score(
"exact_match", predicate=lambda v: v == 0
)
mini_doc = doc.subset(lambda s: s.id in {f.id for f in failures})Use subset (not just filter_samples) when you want the result to be a self-contained document with its sequence list pruned to references in the kept samples.