OpenParable
Skip to content
← Back to skills

Analyze Dataset Token Length

Documents
analyze-dataset-token-length

Analyze Dataset Token Length is an Agent Skill for documents published in the open-thoughts/OpenThoughts-Agent repository on GitHub: a SKILL.md instruction file an AI agent can load to do this task.

Analyze the token length of an OT-Agent conversation-format (ShareGPT-style) dataset — the per-trace distribution (median/p90/max) and/or counts under a token threshold + a metadata predicate (e.g. "task_complete AND < 32768 tokens"). Use when asked how long traces are, how many fit a context window (32k/131k), or to filter a trace dataset by length + a field. Uses the OT-Agent analysis tools + the Qwen3-8B tokenizer. Runs LOCALLY on the Mac (no GPU); full-dataset tokenization of ~10k multi-turn traces takes a few minutes → run it in the background.

View on GitHub ↗
QUICK START

How to use this skill

Bring this guide into your coding agent with a prompt tailored to the tool you use.

  1. Open your project in Codex.
  2. Copy and paste the prompt below.
  3. Review files and risks before approving any action.
Prompt to paste
Inspect the Agent Skill at https://github.com/open-thoughts/OpenThoughts-Agent/blob/HEAD/.claude/skills/analyze-dataset-token-length/SKILL.md and all supporting files. Report provenance, permissions, scripts, dependencies, and security risks. Propose an exact installation into .agents/skills/analyze-dataset-token-length, but wait for my approval before writing files or running scripts.

Copying this prompt does not install or run the skill. Review third-party files before use.

analyze-dataset-token-length

OT-Agent trace datasets are conversation-format (ShareGPT-style): each row is {"conversations": [{"role","content"}, …], + metadata} (some use "messages"; metadata fields are e.g. task, result, run_id, trial_name, model, agent). "Token length of a trace" = the tokenized length of the whole conversation.

The canonical OT-Agent tools (don't reinvent)

  • scripts/analysis/utils.py::extract_conversation_text(record) — the canonical conversation→full-text extractor (handles messages/conversations, content/value/text, list-of-content-parts). Use this to get the text to tokenize.
  • scripts/analysis/context_length_dist.py — token-length distribution across a hardcoded DATASETS list: loads each (load_dataset(..., split="train")), extract_conversation_text per row, batch-tokenizes with the Qwen/Qwen3-8B tokenizer (add_special_tokens=False), prints median / p90 / max, and plots histograms. To analyze a specific dataset, add its HF id to DATASETS and run it (otagent python). This is the go-to for "how long are these traces."
  • scripts/analysis/context_length_compare.py — cross-dataset context-length comparison.

Tokenizer convention

Always Qwen/Qwen3-8B (AutoTokenizer.from_pretrained("Qwen/Qwen3-8B", trust_remote_code=True)). Our trace datasets are Qwen3-8B-tokenized even when named for GLM/Kimi/etc. — those "GLM-4.7-…" models are Qwen3-8B SFTs (see memory reference_glm47_swesmith_is_qwen3_8b); the served model name in a row's model field (e.g. hosted_vllm/<numeric-id>) is NOT a usable tokenizer name.

Two token-count methods — pick by the question

  • plain = tokenizer(extract_conversation_text(row), add_special_tokens=False) — what context_length_dist.py uses; fast; slightly under-counts vs training (no chat-template tokens). Right for distribution/relative comparisons.
  • training-faithful = len(tokenizer.apply_chat_template(conv, tokenize=True, add_generation_prompt=False)) — what an SFT trainer actually tokenizes; use when the question is "does it fit a 32k/131k training window." (Wrap per-row in try/except: if a trace's role shape makes the template raise, fall back to the plain count and tally the fallbacks.)
  • ⚠️ The two can differ by MORE than the wrapper tokens — and in the surprising direction. Qwen3's chat template strips historical <think> blocks from earlier assistant turns, so on thinking-mode traces apply_chat_template can count fewer tokens than plain-concat (which keeps all thinking) — i.e. more traces "fit" under the template. So the "right" count for a < N filter depends on whether your SFT template preserves thinking: default Qwen3 (strips) → optimistic count; a thinking-preserving template (qwen3_thinking_acc.jinja2) → conservative count ≈ plain. Report BOTH and pick by the training template; for a safe "fits 32k" answer use the larger (plain / thinking-preserving) count.

Threshold + metadata filter-count (the common ask)

Recipe: load_dataset (non-streaming) → per row compute (a) the token count and (b) a metadata predicate → count the intersection; report each leg separately so it's auditable. Pattern lives in scripts/analysis/_filter_swesmith_complete_32k.py (a worked one-off — copy + adapt the predicate).

⚠️ The metadata-confound trap (read this before any field predicate)

Instruction text leaks into the trace. Fields like task_complete appear verbatim in the user instruction of EVERY trace (…include "task_complete": true in your response…), so a naive '"task_complete": true' in full_text matches all rows (false 100%). Scope the predicate to the agent's actual emission — i.e. an assistant-role message containing the field, not the prompt:

def agent_complete(conv):
    return any(m.get("role") == "assistant" and '"task_complete": true' in (m.get("content") or "")
               for m in conv)

Always sanity-check the predicate VARIES (not all-true / all-false) before trusting a count — print the per-leg breakdown and an early per-1000-row progress line. (Same caution for any tool-name / status substring: confirm you're matching the agent's output, not the system/user scaffolding.)

How to run

Local, otagent python, HF token sourced; full-dataset tokenization of ~10k multi-turn traces is a few minutes → background it:

source "${DC_AGENT_SECRET_ENV:?set DC_AGENT_SECRET_ENV to the secrets file first}"
/Users/benjaminfeuer/miniconda3/envs/otagent/bin/python scripts/analysis/<script>.py   # run_in_background

First run downloads + caches the parquet (~hundreds of MB). A benign 'NoneType' has no attribute 'ArrowInvalid' on streaming-generator teardown can be ignored (use non-streaming load_dataset anyway).

Worked example — swesmith, "task_complete AND < 32768 tok"

DCAgent2/GLM-4.7-swesmith-sandboxes-with_tests-oracle_verified_120s-maxeps-131k (9437 traces): detect completion via the assistant-scoped "task_complete": true (not the instruction prose), count tokens via apply_chat_template (Qwen3-8B), filter complete AND ct < 32768. Reports three legs — #complete, #<32k, and the intersection — so the filter is auditable. (Early progress 1000/9437: complete=924 / fit32k=886 / both=854 confirmed the predicate varies ≈92%, i.e. the confound was correctly excluded.)