open-interpretability 0.0.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- open_interpretability-0.0.20.dist-info/METADATA +157 -0
- open_interpretability-0.0.20.dist-info/RECORD +256 -0
- open_interpretability-0.0.20.dist-info/WHEEL +4 -0
- open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
- open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
- openinterp/__init__.py +6 -0
- openinterp/agents/__init__.py +25 -0
- openinterp/agents/base.py +229 -0
- openinterp/agents/judge.py +149 -0
- openinterp/agents/proposer.py +471 -0
- openinterp/agents/proposer_profiles.py +79 -0
- openinterp/agents/review_measurements.py +173 -0
- openinterp/agents/reviewer.py +498 -0
- openinterp/agreement.py +63 -0
- openinterp/audit_options.py +79 -0
- openinterp/baseline.py +254 -0
- openinterp/benchmark.py +450 -0
- openinterp/blends.py +239 -0
- openinterp/builder/__init__.py +6 -0
- openinterp/builder/agents.py +260 -0
- openinterp/builder/aligned.py +41 -0
- openinterp/builder/charts.py +411 -0
- openinterp/builder/circuit_pages.py +37 -0
- openinterp/builder/circuits.py +264 -0
- openinterp/builder/compiler.py +495 -0
- openinterp/builder/corpora.py +98 -0
- openinterp/builder/coverage.py +46 -0
- openinterp/builder/dashboard.py +255 -0
- openinterp/builder/disk_memo.py +65 -0
- openinterp/builder/experiment.py +105 -0
- openinterp/builder/heat.py +121 -0
- openinterp/builder/hrefs.py +71 -0
- openinterp/builder/learned_baseline.py +204 -0
- openinterp/builder/model_facts.py +138 -0
- openinterp/builder/neuronpedia_export.py +107 -0
- openinterp/builder/parallel.py +100 -0
- openinterp/builder/parquet_export.py +458 -0
- openinterp/builder/pieces.py +127 -0
- openinterp/builder/pii.py +211 -0
- openinterp/builder/registry_stats.py +144 -0
- openinterp/builder/research.py +109 -0
- openinterp/builder/sections.py +28 -0
- openinterp/builder/significance.py +103 -0
- openinterp/builder/sister_graph.py +180 -0
- openinterp/builder/standing.py +185 -0
- openinterp/builder/statuses.py +34 -0
- openinterp/builder/stopped.py +91 -0
- openinterp/builder/stops.py +30 -0
- openinterp/builder/superpositions.py +102 -0
- openinterp/builder/templates/_agent_role_cells.html +9 -0
- openinterp/builder/templates/_causal_chart.html +67 -0
- openinterp/builder/templates/_circuit.html +8 -0
- openinterp/builder/templates/_dashboard.html +133 -0
- openinterp/builder/templates/_experiment.html +500 -0
- openinterp/builder/templates/_macros.html +82 -0
- openinterp/builder/templates/_patchscope.html +33 -0
- openinterp/builder/templates/_prior_art.html +24 -0
- openinterp/builder/templates/_specificity.html +21 -0
- openinterp/builder/templates/_steering.html +34 -0
- openinterp/builder/templates/_superposition.html +62 -0
- openinterp/builder/templates/_vocab_chart.html +88 -0
- openinterp/builder/templates/_wiring.html +44 -0
- openinterp/builder/templates/about.html +95 -0
- openinterp/builder/templates/agent.html +69 -0
- openinterp/builder/templates/agents.html +80 -0
- openinterp/builder/templates/base.html +103 -0
- openinterp/builder/templates/circuit.html +64 -0
- openinterp/builder/templates/circuits.html +46 -0
- openinterp/builder/templates/experiment.html +21 -0
- openinterp/builder/templates/hub.html +16 -0
- openinterp/builder/templates/index.html +387 -0
- openinterp/builder/templates/layer.html +69 -0
- openinterp/builder/templates/learned-baseline.html +44 -0
- openinterp/builder/templates/model.html +151 -0
- openinterp/builder/templates/models.html +47 -0
- openinterp/builder/templates/neuron.html +234 -0
- openinterp/builder/templates/paper.html +39 -0
- openinterp/builder/templates/redirect.html +1 -0
- openinterp/builder/templates/research.html +31 -0
- openinterp/builder/templates/user.html +54 -0
- openinterp/builder/templates/users.html +39 -0
- openinterp/builder/templates/wiki/experiment.md +271 -0
- openinterp/builder/templates/wiki/index.md +32 -0
- openinterp/builder/templates/wiki/neuron.md +30 -0
- openinterp/builder/templates/wiki/stop.md +36 -0
- openinterp/builder/tree.py +470 -0
- openinterp/builder/users.py +142 -0
- openinterp/builder/wiki.py +427 -0
- openinterp/checks.py +429 -0
- openinterp/cli.py +494 -0
- openinterp/cli_baseline.py +332 -0
- openinterp/cli_benchmark.py +300 -0
- openinterp/cli_build.py +128 -0
- openinterp/cli_check.py +51 -0
- openinterp/cli_concepts.py +76 -0
- openinterp/cli_corpus.py +252 -0
- openinterp/cli_costs.py +257 -0
- openinterp/cli_coverage.py +76 -0
- openinterp/cli_crossref.py +86 -0
- openinterp/cli_errata.py +142 -0
- openinterp/cli_evidence.py +495 -0
- openinterp/cli_evidence_store.py +93 -0
- openinterp/cli_explain.py +101 -0
- openinterp/cli_loop.py +125 -0
- openinterp/cli_memory.py +28 -0
- openinterp/cli_neuronpedia.py +175 -0
- openinterp/cli_pause.py +45 -0
- openinterp/cli_pole_effects.py +133 -0
- openinterp/cli_probing.py +119 -0
- openinterp/cli_registry.py +348 -0
- openinterp/cli_reproduce.py +446 -0
- openinterp/cli_review.py +302 -0
- openinterp/cli_scan.py +223 -0
- openinterp/cli_score.py +446 -0
- openinterp/cli_sisters.py +86 -0
- openinterp/cli_submission.py +217 -0
- openinterp/cli_superpositions.py +118 -0
- openinterp/cli_wiring.py +54 -0
- openinterp/codeid.py +73 -0
- openinterp/concepts.py +400 -0
- openinterp/core/__init__.py +59 -0
- openinterp/core/agent_names.py +76 -0
- openinterp/core/attribution.py +101 -0
- openinterp/core/cache_dirs.py +15 -0
- openinterp/core/causal.py +82 -0
- openinterp/core/confounds.py +103 -0
- openinterp/core/contributors.py +26 -0
- openinterp/core/corpora.py +126 -0
- openinterp/core/costs.py +430 -0
- openinterp/core/draws.py +109 -0
- openinterp/core/errata.py +244 -0
- openinterp/core/evidence.py +160 -0
- openinterp/core/examples.py +93 -0
- openinterp/core/exceptions.py +29 -0
- openinterp/core/explanation.py +130 -0
- openinterp/core/fence.py +42 -0
- openinterp/core/heavy_lock.py +114 -0
- openinterp/core/identifiers.py +198 -0
- openinterp/core/isolation.py +149 -0
- openinterp/core/ledger.py +130 -0
- openinterp/core/licenses.py +28 -0
- openinterp/core/manifold.py +117 -0
- openinterp/core/partners.py +32 -0
- openinterp/core/patchscope.py +59 -0
- openinterp/core/paths.py +40 -0
- openinterp/core/pause.py +96 -0
- openinterp/core/pricing.py +178 -0
- openinterp/core/prior_art.py +44 -0
- openinterp/core/probing.py +146 -0
- openinterp/core/profiles.py +221 -0
- openinterp/core/project.py +28 -0
- openinterp/core/run_environment.py +38 -0
- openinterp/core/schemas.py +484 -0
- openinterp/core/specificity.py +49 -0
- openinterp/core/stats.py +172 -0
- openinterp/core/steering.py +49 -0
- openinterp/core/strict.py +19 -0
- openinterp/core/takedowns.py +136 -0
- openinterp/core/transcripts.py +130 -0
- openinterp/core/wiring.py +80 -0
- openinterp/corpus.py +476 -0
- openinterp/corpus_cache.py +145 -0
- openinterp/corpus_layouts.py +108 -0
- openinterp/corpus_presets.py +168 -0
- openinterp/corpus_rebuild.py +93 -0
- openinterp/coverage.py +155 -0
- openinterp/crossref.py +442 -0
- openinterp/data/__init__.py +9 -0
- openinterp/data/offensive_words.txt +58 -0
- openinterp/data/public_suffix_list.dat +15771 -0
- openinterp/data/sample_corpus.txt +40 -0
- openinterp/draw_steps.py +359 -0
- openinterp/errata_checks.py +129 -0
- openinterp/evidence.py +462 -0
- openinterp/evidence_branch.py +119 -0
- openinterp/evidence_lenses.py +75 -0
- openinterp/experiment_loop.py +357 -0
- openinterp/explain.py +252 -0
- openinterp/half_precision.py +79 -0
- openinterp/harness/__init__.py +37 -0
- openinterp/harness/ane/__init__.py +9 -0
- openinterp/harness/ane/backend.py +324 -0
- openinterp/harness/ane/cache.py +276 -0
- openinterp/harness/ane/chunks.py +163 -0
- openinterp/harness/ane/convert.py +116 -0
- openinterp/harness/ane/forward.py +258 -0
- openinterp/harness/ane/qualified.py +40 -0
- openinterp/harness/annotations.py +329 -0
- openinterp/harness/attribution.py +96 -0
- openinterp/harness/causal.py +264 -0
- openinterp/harness/confounds.py +184 -0
- openinterp/harness/covariance.py +301 -0
- openinterp/harness/crossref.py +195 -0
- openinterp/harness/cuda_devices.py +88 -0
- openinterp/harness/dead.py +124 -0
- openinterp/harness/devices.py +401 -0
- openinterp/harness/dictionaries.py +496 -0
- openinterp/harness/dreaming.py +116 -0
- openinterp/harness/environment.py +64 -0
- openinterp/harness/explanation_scores.py +371 -0
- openinterp/harness/features.py +276 -0
- openinterp/harness/first_position.py +137 -0
- openinterp/harness/heads.py +226 -0
- openinterp/harness/hooks.py +499 -0
- openinterp/harness/input_attribution.py +367 -0
- openinterp/harness/jlens.py +183 -0
- openinterp/harness/layer_cache.py +301 -0
- openinterp/harness/layer_major.py +274 -0
- openinterp/harness/lookahead.py +93 -0
- openinterp/harness/manifold.py +200 -0
- openinterp/harness/memory.py +471 -0
- openinterp/harness/moe.py +134 -0
- openinterp/harness/patcher.py +322 -0
- openinterp/harness/patchscope.py +177 -0
- openinterp/harness/probing.py +383 -0
- openinterp/harness/scan.py +462 -0
- openinterp/harness/scan_backend.py +201 -0
- openinterp/harness/sister_passes.py +463 -0
- openinterp/harness/sisters.py +363 -0
- openinterp/harness/sites.py +101 -0
- openinterp/harness/specificity.py +107 -0
- openinterp/harness/steering.py +159 -0
- openinterp/harness/superpositions.py +116 -0
- openinterp/harness/units.py +203 -0
- openinterp/harness/wiring.py +432 -0
- openinterp/harness/wiring_activations.py +285 -0
- openinterp/langid.py +73 -0
- openinterp/names.py +129 -0
- openinterp/neuronpedia_baseline.py +88 -0
- openinterp/neuronpedia_review.py +148 -0
- openinterp/planner.py +466 -0
- openinterp/planner_effects.py +89 -0
- openinterp/planner_heads.py +92 -0
- openinterp/planner_models.py +174 -0
- openinterp/planner_sisters.py +122 -0
- openinterp/planner_states.py +57 -0
- openinterp/planner_superpositions.py +114 -0
- openinterp/pole_effects.py +102 -0
- openinterp/prior_art.py +143 -0
- openinterp/reproduce.py +450 -0
- openinterp/revision.py +45 -0
- openinterp/runner_cache.py +35 -0
- openinterp/score_controls.py +169 -0
- openinterp/sensitive.py +243 -0
- openinterp/show.py +236 -0
- openinterp/signing.py +209 -0
- openinterp/sisters.py +476 -0
- openinterp/superposition_finders.py +225 -0
- openinterp/sync/__init__.py +5 -0
- openinterp/sync/r2.py +280 -0
- openinterp/syntax.py +399 -0
- openinterp/takedown_checks.py +76 -0
- openinterp/toxicity.py +158 -0
- openinterp/verdict_regression.py +79 -0
- openinterp/wiring_backfill.py +170 -0
- openinterp/wordnet.py +362 -0
openinterp/cli_costs.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""``open-interpretability costs`` and ``open-interpretability usage``: what experiments cost.
|
|
2
|
+
|
|
3
|
+
``open-interpretability costs`` summarises costs from ledgers of commands or from the registry's
|
|
4
|
+
records. ``open-interpretability usage`` records what an external answerer used.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from openinterp.cli import cli
|
|
16
|
+
from openinterp.core.paths import experiments_dir
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_ledger(paths) -> list[dict]:
|
|
20
|
+
"""Every entry of the ledgers at ``paths``; a missing ledger counts as empty."""
|
|
21
|
+
entries = []
|
|
22
|
+
for path in paths:
|
|
23
|
+
if not path.exists():
|
|
24
|
+
continue # an iteration that ran no openinterp command
|
|
25
|
+
for line in path.read_text().splitlines():
|
|
26
|
+
if line.strip():
|
|
27
|
+
entries.append(json.loads(line))
|
|
28
|
+
return entries
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_session(path: Path) -> dict | None:
|
|
32
|
+
"""The result object of ``claude -p --output-format json``, or ``None``."""
|
|
33
|
+
try:
|
|
34
|
+
data = json.loads(path.read_text())
|
|
35
|
+
except (OSError, ValueError):
|
|
36
|
+
return None
|
|
37
|
+
return data if isinstance(data, dict) else None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def reported_usd(entries: list[dict], sessions: list[dict]) -> tuple[float, int]:
|
|
41
|
+
"""Dollars reported by the LLM calls and driving sessions, and the count left unpriced."""
|
|
42
|
+
calls = [c for e in entries for c in e.get("llm_calls", [])]
|
|
43
|
+
priced = [c["cost_usd"] for c in calls if c.get("cost_usd") is not None]
|
|
44
|
+
priced += [s["total_cost_usd"] for s in sessions if s.get("total_cost_usd") is not None]
|
|
45
|
+
unpriced = sum(c.get("cost_usd") is None for c in calls)
|
|
46
|
+
unpriced += sum(s.get("total_cost_usd") is None for s in sessions)
|
|
47
|
+
return sum(priced), unpriced
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def summarise(entries: list[dict], sessions: list[dict]) -> str:
|
|
51
|
+
"""Tables of wall time per command and stage, and of LLM usage, from ledgers and sessions."""
|
|
52
|
+
commands: dict[str, list[float]] = defaultdict(list)
|
|
53
|
+
stages: dict[str, list[float]] = defaultdict(list)
|
|
54
|
+
for e in entries:
|
|
55
|
+
commands[e["command"]].append(e["seconds"])
|
|
56
|
+
for s in e.get("stages", []):
|
|
57
|
+
stages[s["stage"]].append(s["seconds"])
|
|
58
|
+
lines = _table("Commands", commands) + _table("Stages", stages)
|
|
59
|
+
calls: dict[tuple[str, str], list[dict]] = defaultdict(list)
|
|
60
|
+
for e in entries:
|
|
61
|
+
for c in e.get("llm_calls", []):
|
|
62
|
+
calls[(c["agent"], c["model"])].append(c)
|
|
63
|
+
if calls:
|
|
64
|
+
lines.append("LLM calls:")
|
|
65
|
+
for (agent, model), cs in sorted(calls.items()):
|
|
66
|
+
lines.append(f" {agent:<9} {model:<34} ×{len(cs):<3} {_usage(cs)}")
|
|
67
|
+
if sessions:
|
|
68
|
+
lines.append(f"Driver: {_session(sessions)}")
|
|
69
|
+
total = sum(e["seconds"] for e in entries)
|
|
70
|
+
known = [c["cost_usd"] for cs in calls.values() for c in cs if c.get("cost_usd") is not None]
|
|
71
|
+
known += [s["total_cost_usd"] for s in sessions if s.get("total_cost_usd") is not None]
|
|
72
|
+
cost = f", ${sum(known):.2f} reported" if known else ""
|
|
73
|
+
took = f"{total / 60:.1f} min" if total >= 60 else f"{total:.1f} s"
|
|
74
|
+
lines.append(f"Total: {took} in openinterp commands{cost}")
|
|
75
|
+
return "\n".join(lines)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _table(title: str, groups: dict[str, list[float]]) -> list[str]:
|
|
79
|
+
rows = sorted(groups.items(), key=lambda kv: -sum(kv[1]))
|
|
80
|
+
head = [f"{title} (wall time, s):"] if rows else []
|
|
81
|
+
return head + [f" {name:<22} ×{len(secs):<3} {sum(secs):9.1f}" for name, secs in rows]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _usage(calls: list[dict]) -> str:
|
|
85
|
+
def total(key):
|
|
86
|
+
values = [c[key] for c in calls if c.get(key) is not None]
|
|
87
|
+
return sum(values) if values else None
|
|
88
|
+
|
|
89
|
+
parts = []
|
|
90
|
+
if (chars := total("prompt_chars")) is not None:
|
|
91
|
+
parts.append(f"{chars / 1000:.0f}k prompt chars")
|
|
92
|
+
prompt, completion = total("prompt_tokens"), total("completion_tokens")
|
|
93
|
+
if prompt is not None or completion is not None:
|
|
94
|
+
cached = total("cached_tokens")
|
|
95
|
+
hit = f" ({cached:,} cached)" if cached else ""
|
|
96
|
+
parts.append(f"{prompt or 0:,}{hit} + {completion or 0:,} tokens")
|
|
97
|
+
if (cost := total("cost_usd")) is not None:
|
|
98
|
+
parts.append(f"${cost:.3f}")
|
|
99
|
+
if any(c.get("external") and c.get("prompt_tokens") is None for c in calls):
|
|
100
|
+
parts.append("external without a usage file: no token counts")
|
|
101
|
+
return ", ".join(parts)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _session(sessions: list[dict]) -> str:
|
|
105
|
+
usage: dict[str, int] = defaultdict(int)
|
|
106
|
+
for s in sessions:
|
|
107
|
+
for key, value in (s.get("usage") or {}).items():
|
|
108
|
+
if isinstance(value, int):
|
|
109
|
+
usage[key] += value
|
|
110
|
+
minutes = sum(s.get("duration_ms") or 0 for s in sessions) / 60000
|
|
111
|
+
turns = sum(s.get("num_turns") or 0 for s in sessions)
|
|
112
|
+
read, written = usage["cache_read_input_tokens"], usage["cache_creation_input_tokens"]
|
|
113
|
+
tokens = (
|
|
114
|
+
f"{usage['input_tokens'] + read + written:,} input ({read:,} cache reads) + "
|
|
115
|
+
f"{usage['output_tokens']:,} output tokens"
|
|
116
|
+
)
|
|
117
|
+
costs = [s["total_cost_usd"] for s in sessions if s.get("total_cost_usd") is not None]
|
|
118
|
+
cost = f", ${sum(costs):.2f}" if costs else ""
|
|
119
|
+
n = len(sessions)
|
|
120
|
+
return f"{n} session{'s' * (n != 1)}, {minutes:.1f} min, {turns} turns, {tokens}{cost}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def registry_costs(wiki_dir: Path) -> str:
|
|
124
|
+
"""Cost per experiment by role and model, over the records in ``wiki_dir``."""
|
|
125
|
+
from openinterp.builder.compiler import load_records
|
|
126
|
+
from openinterp.core.costs import by_agent
|
|
127
|
+
|
|
128
|
+
records = load_records(experiments_dir(wiki_dir))
|
|
129
|
+
rows = by_agent(
|
|
130
|
+
(
|
|
131
|
+
r.verdict.status if r.verdict else None,
|
|
132
|
+
r.submission.provenance and r.submission.provenance.costs,
|
|
133
|
+
)
|
|
134
|
+
for r in records
|
|
135
|
+
)
|
|
136
|
+
if not rows:
|
|
137
|
+
return f"No LLM calls recorded in {len(records)} records."
|
|
138
|
+
|
|
139
|
+
def usd(v):
|
|
140
|
+
return f"${v:.3f}" if v is not None else "–"
|
|
141
|
+
|
|
142
|
+
lines = [
|
|
143
|
+
f"LLM cost per experiment ({len(records)} records; priced = every call has a cost):",
|
|
144
|
+
f" {'role':<9} {'model':<40} {'priced':>11} {'mean':>8} {'per VERIFIED':>13}",
|
|
145
|
+
]
|
|
146
|
+
for r in rows:
|
|
147
|
+
priced = f"{r['priced']}/{r['experiments']}"
|
|
148
|
+
lines.append(
|
|
149
|
+
f" {r['agent']:<9} {r['model']:<40} {priced:>11} {usd(r['mean_usd']):>8} "
|
|
150
|
+
f"{usd(r['usd_per_verified']):>13}"
|
|
151
|
+
)
|
|
152
|
+
return "\n".join(lines)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@cli.command()
|
|
156
|
+
@click.argument("ledgers", nargs=-1, type=click.Path(dir_okay=False, path_type=Path))
|
|
157
|
+
@click.option(
|
|
158
|
+
"--session",
|
|
159
|
+
"sessions",
|
|
160
|
+
multiple=True,
|
|
161
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
162
|
+
help="`claude -p --output-format json` output of the driving session (repeatable).",
|
|
163
|
+
)
|
|
164
|
+
@click.option(
|
|
165
|
+
"--wiki-dir",
|
|
166
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
167
|
+
help="Summarise the registry's records instead: LLM cost per experiment by role and "
|
|
168
|
+
"model, mean and per VERIFIED result.",
|
|
169
|
+
)
|
|
170
|
+
@click.option(
|
|
171
|
+
"--usd",
|
|
172
|
+
is_flag=True,
|
|
173
|
+
help="Print only the dollars reported, as one number. The count of unpriced calls "
|
|
174
|
+
"goes to stderr. scripts/experiment-loop.sh compares it with its budgets.",
|
|
175
|
+
)
|
|
176
|
+
def costs(ledgers, sessions, wiki_dir, usd):
|
|
177
|
+
"""Summarise wall time and LLM usage, from ledgers or from the registry.
|
|
178
|
+
|
|
179
|
+
The ledgers are $OPENINTERP_COSTS_LOG files. With --wiki-dir, it gives the registry's
|
|
180
|
+
LLM costs by role and model.
|
|
181
|
+
|
|
182
|
+
Set OPENINTERP_COSTS_LOG=<file> and every openinterp command appends what it measured
|
|
183
|
+
there: its wall time, its stages and its LLM calls. Records hold the same numbers for
|
|
184
|
+
their own experiment, in `submission.provenance.costs`. Reported, never gated.
|
|
185
|
+
"""
|
|
186
|
+
if wiki_dir is not None:
|
|
187
|
+
if ledgers or sessions:
|
|
188
|
+
raise click.UsageError("--wiki-dir summarises records; drop the ledgers.")
|
|
189
|
+
click.echo(registry_costs(wiki_dir))
|
|
190
|
+
return
|
|
191
|
+
if not ledgers and not sessions:
|
|
192
|
+
if usd: # no experiment has run yet, so nothing is spent
|
|
193
|
+
click.echo("0.0000")
|
|
194
|
+
return
|
|
195
|
+
raise click.UsageError("Pass at least one ledger, --session file or --wiki-dir.")
|
|
196
|
+
found = [s for s in (read_session(p) for p in sessions) if s is not None]
|
|
197
|
+
if usd:
|
|
198
|
+
dollars, unpriced = reported_usd(read_ledger(ledgers), found)
|
|
199
|
+
if unpriced:
|
|
200
|
+
click.echo(f"{unpriced} unpriced calls or sessions are not counted", err=True)
|
|
201
|
+
click.echo(f"{dollars:.4f}")
|
|
202
|
+
return
|
|
203
|
+
click.echo(summarise(read_ledger(ledgers), found))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@cli.command()
|
|
207
|
+
@click.argument("agent")
|
|
208
|
+
@click.option(
|
|
209
|
+
"--answer",
|
|
210
|
+
required=True,
|
|
211
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
212
|
+
help="The answer the subagent wrote, e.g. exchange/ev.hypothesis.json.",
|
|
213
|
+
)
|
|
214
|
+
def usage(agent, answer):
|
|
215
|
+
"""Record a Claude Code subagent's token usage next to the answer it wrote.
|
|
216
|
+
|
|
217
|
+
AGENT is the agent ID that Claude Code reports when the subagent finishes. It can also
|
|
218
|
+
be the path of the subagent's transcript:
|
|
219
|
+
~/.claude/projects/<project>/<session>/subagents/agent-<id>.jsonl.
|
|
220
|
+
Writes <answer>.usage.json. `audit --hypothesis`, `review --probes` and
|
|
221
|
+
`score --task` read it to record the call's tokens and cost. They price it at list
|
|
222
|
+
price for the model that they record. That is after $OPENINTERP_MODEL_ALIASES, so a
|
|
223
|
+
proxied model is priced as the model that actually answered.
|
|
224
|
+
"""
|
|
225
|
+
from openinterp.core.costs import usage_path
|
|
226
|
+
from openinterp.core.paths import PROBES_SUFFIX
|
|
227
|
+
from openinterp.core.transcripts import (
|
|
228
|
+
find_transcript,
|
|
229
|
+
forbidden_reads,
|
|
230
|
+
usage_from_transcript,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
transcript = find_transcript(agent)
|
|
235
|
+
except FileNotFoundError as exc:
|
|
236
|
+
raise click.BadParameter(str(exc), param_hint="AGENT") from exc
|
|
237
|
+
found = usage_from_transcript(transcript)
|
|
238
|
+
if answer.name.endswith(PROBES_SUFFIX): # a reviewer's answer: check its isolation
|
|
239
|
+
read, tried = forbidden_reads(transcript)
|
|
240
|
+
found = found.model_copy(update={"forbidden_reads": read})
|
|
241
|
+
if tried:
|
|
242
|
+
click.echo(f"The reviewer tried to read blocked paths: {', '.join(tried)}", err=True)
|
|
243
|
+
if found.forbidden_reads:
|
|
244
|
+
click.echo(
|
|
245
|
+
"warning: the reviewer read blocked paths, so `review --probes` will refuse "
|
|
246
|
+
f"{answer}: {', '.join(found.forbidden_reads)}. Run a new reviewer.",
|
|
247
|
+
err=True,
|
|
248
|
+
)
|
|
249
|
+
out = usage_path(answer)
|
|
250
|
+
out.write_text(found.model_dump_json(indent=2) + "\n")
|
|
251
|
+
tokens = found.input_tokens + found.cache_read_tokens
|
|
252
|
+
tokens += found.cache_write_5m_tokens + found.cache_write_1h_tokens
|
|
253
|
+
click.echo(
|
|
254
|
+
f"{found.model}: {found.requests} requests, {tokens:,} input "
|
|
255
|
+
f"({found.cache_read_tokens:,} cache reads) + {found.output_tokens:,} output tokens. "
|
|
256
|
+
f"Wrote {out}"
|
|
257
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""The ``coverage`` command: how much of each layer's effect the VERIFIED poles keep."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from openinterp.cli import cli, wiki_dir_option, with_model_options
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@cli.command()
|
|
15
|
+
@click.option("--model", "model_name", required=True, help="Hugging Face model ID.")
|
|
16
|
+
@click.option(
|
|
17
|
+
"--corpus",
|
|
18
|
+
type=click.Path(exists=True, path_type=Path),
|
|
19
|
+
default="corpus.jsonl",
|
|
20
|
+
show_default=True,
|
|
21
|
+
help="Text for the windows.",
|
|
22
|
+
)
|
|
23
|
+
@click.option(
|
|
24
|
+
"--windows",
|
|
25
|
+
default=64,
|
|
26
|
+
show_default=True,
|
|
27
|
+
type=click.IntRange(1),
|
|
28
|
+
help="Windows to fit the means on, and as many again to measure the loss on.",
|
|
29
|
+
)
|
|
30
|
+
@click.option("--window-tokens", default=128, show_default=True, type=click.IntRange(8))
|
|
31
|
+
@click.option("--batch", default=4, show_default=True, type=click.IntRange(1))
|
|
32
|
+
@click.option("--seed", default=0, show_default=True, type=int)
|
|
33
|
+
@click.option("--layers", help="Comma-separated layers (default: every layer).")
|
|
34
|
+
@click.option(
|
|
35
|
+
"--out",
|
|
36
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
37
|
+
default="docs/analyses/registry-coverage.jsonl",
|
|
38
|
+
show_default=True,
|
|
39
|
+
help="Rows are appended here, one per layer.",
|
|
40
|
+
)
|
|
41
|
+
@wiki_dir_option()
|
|
42
|
+
@with_model_options
|
|
43
|
+
def coverage(model_name, corpus, windows, window_tokens, batch, seed, layers, out, wiki_dir,
|
|
44
|
+
device, dtype): # fmt: skip
|
|
45
|
+
"""Measure the share of each MLP layer's loss effect that the VERIFIED poles keep.
|
|
46
|
+
|
|
47
|
+
Reported, not gated. See openinterp/coverage.py for the method.
|
|
48
|
+
"""
|
|
49
|
+
from openinterp import __version__
|
|
50
|
+
from openinterp.cli import _load_runner
|
|
51
|
+
from openinterp.cli_evidence import read_corpus
|
|
52
|
+
from openinterp.coverage import layer_row, mean_loss, verified_poles
|
|
53
|
+
from openinterp.coverage import windows as draw_windows
|
|
54
|
+
|
|
55
|
+
verified = verified_poles(model_name, wiki_dir)
|
|
56
|
+
runner = _load_runner(model_name, device, dtype)
|
|
57
|
+
texts, corpus_name, corpus_sha, _ = read_corpus(corpus)
|
|
58
|
+
fit, test = draw_windows(runner, texts, windows, window_tokens, seed)
|
|
59
|
+
chosen = [int(x) for x in layers.split(",")] if layers else range(runner.n_layers)
|
|
60
|
+
clean = mean_loss(runner, test, batch)
|
|
61
|
+
click.echo(f"Clean loss {clean:.4f} on {test.numel()} tokens", err=True)
|
|
62
|
+
meta = {"model": model_name, "corpus": corpus_name, "corpus_sha256": corpus_sha,
|
|
63
|
+
"windows": windows, "window_tokens": window_tokens, "seed": seed,
|
|
64
|
+
"dtype": dtype, "openinterp": __version__,
|
|
65
|
+
"date": datetime.now(UTC).strftime("%Y-%m-%d")} # fmt: skip
|
|
66
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
for i, layer in enumerate(chosen, 1):
|
|
68
|
+
row = meta | layer_row(
|
|
69
|
+
runner, fit, test, layer, verified.get(layer, set()), clean, batch, seed
|
|
70
|
+
)
|
|
71
|
+
with out.open("a") as fh:
|
|
72
|
+
fh.write(json.dumps(row) + "\n")
|
|
73
|
+
share = row.get("recovered")
|
|
74
|
+
click.echo(f"[{i}/{len(chosen)}] L{layer}: {row['verified_poles']} VERIFIED poles, "
|
|
75
|
+
f"recovered {'—' if share is None else f'{share:.3f}'}"
|
|
76
|
+
f" (random {row.get('recovered_random') or 0:.3f})", err=True) # fmt: skip
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""``open-interpretability crossref`` and ``open-interpretability aligned-latents``.
|
|
2
|
+
|
|
3
|
+
They compare neuron poles with published dictionaries. See :mod:`openinterp.crossref`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from openinterp.cli import _load_runner, cli, wiki_dir_option
|
|
13
|
+
from openinterp.crossref import DEFAULT_PATH
|
|
14
|
+
|
|
15
|
+
out_option = click.option(
|
|
16
|
+
"--out",
|
|
17
|
+
"path",
|
|
18
|
+
default=DEFAULT_PATH,
|
|
19
|
+
show_default=True,
|
|
20
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
21
|
+
help="JSONL of alignment rows, one per pole and dictionary.",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cli.command("crossref")
|
|
26
|
+
@wiki_dir_option()
|
|
27
|
+
@out_option
|
|
28
|
+
@click.option("-k", "k", default=5, show_default=True, help="Latents kept per side.")
|
|
29
|
+
@click.option("--device", default="auto", show_default=True, help="cpu, cuda, mps or auto.")
|
|
30
|
+
def crossref_command(wiki_dir, path, k, device):
|
|
31
|
+
"""Find the dictionary latents aligned with every archived neuron pole's weights.
|
|
32
|
+
|
|
33
|
+
Covers each pole whose model has a dictionary preset at its layer; rows already in
|
|
34
|
+
--out are skipped, so a run can resume. Weights only: no text is run.
|
|
35
|
+
"""
|
|
36
|
+
from openinterp.builder.wiki import load_archive
|
|
37
|
+
from openinterp.crossref import crossref, sort_rows
|
|
38
|
+
|
|
39
|
+
def load(model_name, revision):
|
|
40
|
+
return _load_runner(model_name, device, "float32", revision)
|
|
41
|
+
|
|
42
|
+
wrote = crossref(load_archive(wiki_dir), load, path, k=k, echo=click.echo)
|
|
43
|
+
if path.exists():
|
|
44
|
+
sort_rows(path)
|
|
45
|
+
click.echo(f"Aligned {wrote} pole–dictionary pairs → {path}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@cli.command("aligned-latents")
|
|
49
|
+
@click.argument("slug")
|
|
50
|
+
@out_option
|
|
51
|
+
@wiki_dir_option()
|
|
52
|
+
def aligned_latents(slug, path, wiki_dir):
|
|
53
|
+
"""Show a pole's aligned latents with their descriptions.
|
|
54
|
+
|
|
55
|
+
The pole is a slug such as Qwen--Qwen2.5-0.5B/L3/N12-positive. The descriptions come
|
|
56
|
+
from this registry or Neuronpedia.
|
|
57
|
+
"""
|
|
58
|
+
from openinterp.builder.wiki import load_archive
|
|
59
|
+
from openinterp.crossref import describe, load_rows, neuronpedia_url, registry_explanations
|
|
60
|
+
|
|
61
|
+
rows = load_rows(path).get(slug)
|
|
62
|
+
if not rows:
|
|
63
|
+
raise click.ClickException(f"no alignment rows for {slug} in {path}; run crossref")
|
|
64
|
+
registry = registry_explanations(load_archive(wiki_dir))
|
|
65
|
+
described = {
|
|
66
|
+
(id(d["row"]), d["side"], d["match"].feature_idx): d for d in describe(rows, registry)
|
|
67
|
+
}
|
|
68
|
+
for row in rows:
|
|
69
|
+
for side_name in ("writes", "reads"):
|
|
70
|
+
side = getattr(row, side_name)
|
|
71
|
+
if side is None:
|
|
72
|
+
continue
|
|
73
|
+
click.echo(
|
|
74
|
+
f"{row.dictionary} {side_name} (chance {side.chance:.2f}, "
|
|
75
|
+
f"best beats {side.percentile:.0%} of the layer's neurons)"
|
|
76
|
+
)
|
|
77
|
+
for m in side.matches:
|
|
78
|
+
d = described.get((id(row), side_name, m.feature_idx))
|
|
79
|
+
channel = f" {m.channel}" if m.channel and side_name == "reads" else ""
|
|
80
|
+
text = (d and d["description"]) or ("below chance" if d is None else "—")
|
|
81
|
+
if d and d["source"]:
|
|
82
|
+
text += f" [{d['source']}]"
|
|
83
|
+
url = neuronpedia_url(row, m.feature_idx)
|
|
84
|
+
click.echo(f" F{m.feature_idx:<6} {m.cosine:+.3f}{channel} {text}")
|
|
85
|
+
if url:
|
|
86
|
+
click.echo(f" {url}")
|
openinterp/cli_errata.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""``open-interpretability erratum draft``: write an erratum's line from a rule (issue #3271).
|
|
2
|
+
|
|
3
|
+
A maintainer gives the bug, the fix commit and the records it covers: a rule, listed
|
|
4
|
+
records, or both. The command lists every record that the erratum covers, so that the
|
|
5
|
+
maintainer checks a list and not a hand-written JSON line. It checks the draft as
|
|
6
|
+
``open-interpretability check`` will check the file (:mod:`openinterp.errata_checks`).
|
|
7
|
+
Then it prints the line. ``--write`` appends the line to ``wiki/errata.jsonl``. Signing
|
|
8
|
+
stays a separate step, ``open-interpretability sign wiki/errata.jsonl``, after the
|
|
9
|
+
maintainer reads the list.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import subprocess
|
|
16
|
+
from datetime import date as Date
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
from openinterp.cli import cli, wiki_dir_option
|
|
22
|
+
|
|
23
|
+
KINDS = ("mlp_neuron_pole", "attn_head", "dictionary_feature")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@cli.group()
|
|
27
|
+
def erratum() -> None:
|
|
28
|
+
"""Draft errata for signed records (docs/PROTOCOL.md §5)."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _edit(text: str) -> dict:
|
|
32
|
+
"""``PATH=JSON`` as an edit; a value that isn't JSON is a string."""
|
|
33
|
+
path, sep, value = text.partition("=")
|
|
34
|
+
if not sep:
|
|
35
|
+
raise click.BadParameter(f"{text!r} is not PATH=VALUE", param_hint="--edit")
|
|
36
|
+
try:
|
|
37
|
+
return {"path": path, "value": json.loads(value)}
|
|
38
|
+
except json.JSONDecodeError:
|
|
39
|
+
return {"path": path, "value": value}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _rule(**fields) -> dict | None:
|
|
43
|
+
"""The rule's fields that were given, or ``None`` when none was."""
|
|
44
|
+
rule = {k: (list(v) if isinstance(v, tuple) else v) for k, v in fields.items()}
|
|
45
|
+
rule = {k: v for k, v in rule.items() if v not in (None, [])}
|
|
46
|
+
return rule or None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve_commit(ref: str) -> str | None:
|
|
50
|
+
"""The full hash of the commit that ``ref`` names here, or ``None`` if there is none."""
|
|
51
|
+
done = subprocess.run(
|
|
52
|
+
["git", "rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}"],
|
|
53
|
+
capture_output=True, text=True, check=False,
|
|
54
|
+
) # fmt: skip
|
|
55
|
+
return done.stdout.strip() if done.returncode == 0 else None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def draft_erratum(
|
|
59
|
+
wiki_dir: Path, fields: dict, resolve=resolve_commit
|
|
60
|
+
) -> tuple[object, list[tuple[str, object]]]:
|
|
61
|
+
"""The erratum that ``fields`` describe, and the records it covers as ``(ref, record)``.
|
|
62
|
+
|
|
63
|
+
Raises ``click.ClickException`` for each way ``open-interpretability check`` would
|
|
64
|
+
refuse it. These are an id already used, a fix commit that doesn't exist, and a listed
|
|
65
|
+
record that isn't in the archive. Also no record covered, or an edit that doesn't apply.
|
|
66
|
+
``fields["fixed_in"]`` can be any ref, such as a branch or ``HEAD``. The erratum holds
|
|
67
|
+
the hash of its commit.
|
|
68
|
+
"""
|
|
69
|
+
from pydantic import ValidationError
|
|
70
|
+
|
|
71
|
+
from openinterp.builder.wiki import load_archive_paths
|
|
72
|
+
from openinterp.core.errata import Erratum, apply_errata, load_errata, record_ref
|
|
73
|
+
|
|
74
|
+
commit = resolve(fields["fixed_in"])
|
|
75
|
+
if commit is None:
|
|
76
|
+
raise click.ClickException(f"--fixed-in {fields['fixed_in']} is not a commit here")
|
|
77
|
+
try:
|
|
78
|
+
draft = Erratum.model_validate_json(json.dumps(fields | {"fixed_in": commit}))
|
|
79
|
+
except ValidationError as exc:
|
|
80
|
+
raise click.ClickException(f"the erratum is not valid: {exc}") from exc
|
|
81
|
+
if draft.id in {e.id for e in load_errata(wiki_dir)}:
|
|
82
|
+
raise click.ClickException(f"erratum id {draft.id!r} is already in the errata file")
|
|
83
|
+
archive = {record_ref(r): r for _, r in load_archive_paths(wiki_dir)}
|
|
84
|
+
missing = [ref for ref in draft.records if ref not in archive]
|
|
85
|
+
if missing:
|
|
86
|
+
raise click.ClickException(f"listed records not in the archive: {', '.join(missing)}")
|
|
87
|
+
covered = [(ref, r) for ref, r in sorted(archive.items()) if draft.covers(r)]
|
|
88
|
+
if not covered:
|
|
89
|
+
raise click.ClickException("the erratum covers no record")
|
|
90
|
+
for ref, record in covered:
|
|
91
|
+
try:
|
|
92
|
+
apply_errata(record, [draft])
|
|
93
|
+
except ValueError as exc:
|
|
94
|
+
raise click.ClickException(f"{ref}: {exc}") from exc
|
|
95
|
+
return draft, covered
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@erratum.command("draft")
|
|
99
|
+
@wiki_dir_option()
|
|
100
|
+
@click.option("--id", "erratum_id", required=True, help="Unique id: lower case and hyphens.")
|
|
101
|
+
@click.option("--title", required=True, help="One line for the banner.")
|
|
102
|
+
@click.option("--bug", required=True, help="What was wrong, and what it did to records.")
|
|
103
|
+
@click.option("--fixed-in", required=True, help="The commit that fixed the bug.")
|
|
104
|
+
@click.option("--date", "day", default=None, help="The erratum's date (default today).")
|
|
105
|
+
@click.option("--record", "records", multiple=True, help="A record path in wiki/experiments/.")
|
|
106
|
+
@click.option("--model", "models", multiple=True, help="Rule: a model name.")
|
|
107
|
+
@click.option("--kind", "kinds", multiple=True, type=click.Choice(KINDS), help="Rule: unit kind.")
|
|
108
|
+
@click.option("--protocol-version", "protocol_versions", multiple=True, help="Rule: version.")
|
|
109
|
+
@click.option("--harness-version", "harness_versions", multiple=True, help="Rule: version.")
|
|
110
|
+
@click.option("--reviewed-from", help="Rule: first verdict time that matches (ISO, with zone).")
|
|
111
|
+
@click.option("--reviewed-before", help="Rule: verdict time from which records don't match.")
|
|
112
|
+
@click.option("--edit", "edits", multiple=True, help="PATH=JSON: a corrected field.")
|
|
113
|
+
@click.option("--set-aside", is_flag=True, help="The records count toward no status.")
|
|
114
|
+
@click.option("--write", is_flag=True, help="Append the line to wiki/errata.jsonl.")
|
|
115
|
+
def draft(wiki_dir, erratum_id, title, bug, fixed_in, day, records, edits, set_aside, write,
|
|
116
|
+
**rule): # fmt: skip
|
|
117
|
+
"""List the records that an erratum covers, check it, and print its line.
|
|
118
|
+
|
|
119
|
+
Give a rule (--model, --kind, --protocol-version, --harness-version, --reviewed-from,
|
|
120
|
+
--reviewed-before), --record paths, or both. Give --edit corrections or --set-aside.
|
|
121
|
+
Read the list, then sign the file with `open-interpretability sign wiki/errata.jsonl`.
|
|
122
|
+
"""
|
|
123
|
+
from openinterp.core.errata import errata_path
|
|
124
|
+
|
|
125
|
+
fields = {
|
|
126
|
+
"id": erratum_id, "date": day or Date.today().isoformat(), "title": title, "bug": bug,
|
|
127
|
+
"fixed_in": fixed_in, "edits": [_edit(e) for e in edits], "set_aside": set_aside,
|
|
128
|
+
"records": list(records), "rule": _rule(**rule),
|
|
129
|
+
} # fmt: skip
|
|
130
|
+
found, covered = draft_erratum(wiki_dir, fields)
|
|
131
|
+
for ref, record in covered:
|
|
132
|
+
verdict = record.verdict
|
|
133
|
+
status, when = (verdict.status, verdict.timestamp.isoformat()) if verdict else ("-", "-")
|
|
134
|
+
click.echo(f" {ref} {status} {when}")
|
|
135
|
+
click.echo(f"The erratum covers {len(covered)} records.")
|
|
136
|
+
line = found.model_dump_json(exclude_defaults=True)
|
|
137
|
+
click.echo(line)
|
|
138
|
+
if write:
|
|
139
|
+
path = errata_path(wiki_dir)
|
|
140
|
+
with path.open("a", encoding="utf-8") as f:
|
|
141
|
+
f.write(line + "\n")
|
|
142
|
+
click.echo(f"Appended to {path}. Read the list, then: open-interpretability sign {path}")
|