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.
Files changed (256) hide show
  1. open_interpretability-0.0.20.dist-info/METADATA +157 -0
  2. open_interpretability-0.0.20.dist-info/RECORD +256 -0
  3. open_interpretability-0.0.20.dist-info/WHEEL +4 -0
  4. open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
  5. open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
  6. openinterp/__init__.py +6 -0
  7. openinterp/agents/__init__.py +25 -0
  8. openinterp/agents/base.py +229 -0
  9. openinterp/agents/judge.py +149 -0
  10. openinterp/agents/proposer.py +471 -0
  11. openinterp/agents/proposer_profiles.py +79 -0
  12. openinterp/agents/review_measurements.py +173 -0
  13. openinterp/agents/reviewer.py +498 -0
  14. openinterp/agreement.py +63 -0
  15. openinterp/audit_options.py +79 -0
  16. openinterp/baseline.py +254 -0
  17. openinterp/benchmark.py +450 -0
  18. openinterp/blends.py +239 -0
  19. openinterp/builder/__init__.py +6 -0
  20. openinterp/builder/agents.py +260 -0
  21. openinterp/builder/aligned.py +41 -0
  22. openinterp/builder/charts.py +411 -0
  23. openinterp/builder/circuit_pages.py +37 -0
  24. openinterp/builder/circuits.py +264 -0
  25. openinterp/builder/compiler.py +495 -0
  26. openinterp/builder/corpora.py +98 -0
  27. openinterp/builder/coverage.py +46 -0
  28. openinterp/builder/dashboard.py +255 -0
  29. openinterp/builder/disk_memo.py +65 -0
  30. openinterp/builder/experiment.py +105 -0
  31. openinterp/builder/heat.py +121 -0
  32. openinterp/builder/hrefs.py +71 -0
  33. openinterp/builder/learned_baseline.py +204 -0
  34. openinterp/builder/model_facts.py +138 -0
  35. openinterp/builder/neuronpedia_export.py +107 -0
  36. openinterp/builder/parallel.py +100 -0
  37. openinterp/builder/parquet_export.py +458 -0
  38. openinterp/builder/pieces.py +127 -0
  39. openinterp/builder/pii.py +211 -0
  40. openinterp/builder/registry_stats.py +144 -0
  41. openinterp/builder/research.py +109 -0
  42. openinterp/builder/sections.py +28 -0
  43. openinterp/builder/significance.py +103 -0
  44. openinterp/builder/sister_graph.py +180 -0
  45. openinterp/builder/standing.py +185 -0
  46. openinterp/builder/statuses.py +34 -0
  47. openinterp/builder/stopped.py +91 -0
  48. openinterp/builder/stops.py +30 -0
  49. openinterp/builder/superpositions.py +102 -0
  50. openinterp/builder/templates/_agent_role_cells.html +9 -0
  51. openinterp/builder/templates/_causal_chart.html +67 -0
  52. openinterp/builder/templates/_circuit.html +8 -0
  53. openinterp/builder/templates/_dashboard.html +133 -0
  54. openinterp/builder/templates/_experiment.html +500 -0
  55. openinterp/builder/templates/_macros.html +82 -0
  56. openinterp/builder/templates/_patchscope.html +33 -0
  57. openinterp/builder/templates/_prior_art.html +24 -0
  58. openinterp/builder/templates/_specificity.html +21 -0
  59. openinterp/builder/templates/_steering.html +34 -0
  60. openinterp/builder/templates/_superposition.html +62 -0
  61. openinterp/builder/templates/_vocab_chart.html +88 -0
  62. openinterp/builder/templates/_wiring.html +44 -0
  63. openinterp/builder/templates/about.html +95 -0
  64. openinterp/builder/templates/agent.html +69 -0
  65. openinterp/builder/templates/agents.html +80 -0
  66. openinterp/builder/templates/base.html +103 -0
  67. openinterp/builder/templates/circuit.html +64 -0
  68. openinterp/builder/templates/circuits.html +46 -0
  69. openinterp/builder/templates/experiment.html +21 -0
  70. openinterp/builder/templates/hub.html +16 -0
  71. openinterp/builder/templates/index.html +387 -0
  72. openinterp/builder/templates/layer.html +69 -0
  73. openinterp/builder/templates/learned-baseline.html +44 -0
  74. openinterp/builder/templates/model.html +151 -0
  75. openinterp/builder/templates/models.html +47 -0
  76. openinterp/builder/templates/neuron.html +234 -0
  77. openinterp/builder/templates/paper.html +39 -0
  78. openinterp/builder/templates/redirect.html +1 -0
  79. openinterp/builder/templates/research.html +31 -0
  80. openinterp/builder/templates/user.html +54 -0
  81. openinterp/builder/templates/users.html +39 -0
  82. openinterp/builder/templates/wiki/experiment.md +271 -0
  83. openinterp/builder/templates/wiki/index.md +32 -0
  84. openinterp/builder/templates/wiki/neuron.md +30 -0
  85. openinterp/builder/templates/wiki/stop.md +36 -0
  86. openinterp/builder/tree.py +470 -0
  87. openinterp/builder/users.py +142 -0
  88. openinterp/builder/wiki.py +427 -0
  89. openinterp/checks.py +429 -0
  90. openinterp/cli.py +494 -0
  91. openinterp/cli_baseline.py +332 -0
  92. openinterp/cli_benchmark.py +300 -0
  93. openinterp/cli_build.py +128 -0
  94. openinterp/cli_check.py +51 -0
  95. openinterp/cli_concepts.py +76 -0
  96. openinterp/cli_corpus.py +252 -0
  97. openinterp/cli_costs.py +257 -0
  98. openinterp/cli_coverage.py +76 -0
  99. openinterp/cli_crossref.py +86 -0
  100. openinterp/cli_errata.py +142 -0
  101. openinterp/cli_evidence.py +495 -0
  102. openinterp/cli_evidence_store.py +93 -0
  103. openinterp/cli_explain.py +101 -0
  104. openinterp/cli_loop.py +125 -0
  105. openinterp/cli_memory.py +28 -0
  106. openinterp/cli_neuronpedia.py +175 -0
  107. openinterp/cli_pause.py +45 -0
  108. openinterp/cli_pole_effects.py +133 -0
  109. openinterp/cli_probing.py +119 -0
  110. openinterp/cli_registry.py +348 -0
  111. openinterp/cli_reproduce.py +446 -0
  112. openinterp/cli_review.py +302 -0
  113. openinterp/cli_scan.py +223 -0
  114. openinterp/cli_score.py +446 -0
  115. openinterp/cli_sisters.py +86 -0
  116. openinterp/cli_submission.py +217 -0
  117. openinterp/cli_superpositions.py +118 -0
  118. openinterp/cli_wiring.py +54 -0
  119. openinterp/codeid.py +73 -0
  120. openinterp/concepts.py +400 -0
  121. openinterp/core/__init__.py +59 -0
  122. openinterp/core/agent_names.py +76 -0
  123. openinterp/core/attribution.py +101 -0
  124. openinterp/core/cache_dirs.py +15 -0
  125. openinterp/core/causal.py +82 -0
  126. openinterp/core/confounds.py +103 -0
  127. openinterp/core/contributors.py +26 -0
  128. openinterp/core/corpora.py +126 -0
  129. openinterp/core/costs.py +430 -0
  130. openinterp/core/draws.py +109 -0
  131. openinterp/core/errata.py +244 -0
  132. openinterp/core/evidence.py +160 -0
  133. openinterp/core/examples.py +93 -0
  134. openinterp/core/exceptions.py +29 -0
  135. openinterp/core/explanation.py +130 -0
  136. openinterp/core/fence.py +42 -0
  137. openinterp/core/heavy_lock.py +114 -0
  138. openinterp/core/identifiers.py +198 -0
  139. openinterp/core/isolation.py +149 -0
  140. openinterp/core/ledger.py +130 -0
  141. openinterp/core/licenses.py +28 -0
  142. openinterp/core/manifold.py +117 -0
  143. openinterp/core/partners.py +32 -0
  144. openinterp/core/patchscope.py +59 -0
  145. openinterp/core/paths.py +40 -0
  146. openinterp/core/pause.py +96 -0
  147. openinterp/core/pricing.py +178 -0
  148. openinterp/core/prior_art.py +44 -0
  149. openinterp/core/probing.py +146 -0
  150. openinterp/core/profiles.py +221 -0
  151. openinterp/core/project.py +28 -0
  152. openinterp/core/run_environment.py +38 -0
  153. openinterp/core/schemas.py +484 -0
  154. openinterp/core/specificity.py +49 -0
  155. openinterp/core/stats.py +172 -0
  156. openinterp/core/steering.py +49 -0
  157. openinterp/core/strict.py +19 -0
  158. openinterp/core/takedowns.py +136 -0
  159. openinterp/core/transcripts.py +130 -0
  160. openinterp/core/wiring.py +80 -0
  161. openinterp/corpus.py +476 -0
  162. openinterp/corpus_cache.py +145 -0
  163. openinterp/corpus_layouts.py +108 -0
  164. openinterp/corpus_presets.py +168 -0
  165. openinterp/corpus_rebuild.py +93 -0
  166. openinterp/coverage.py +155 -0
  167. openinterp/crossref.py +442 -0
  168. openinterp/data/__init__.py +9 -0
  169. openinterp/data/offensive_words.txt +58 -0
  170. openinterp/data/public_suffix_list.dat +15771 -0
  171. openinterp/data/sample_corpus.txt +40 -0
  172. openinterp/draw_steps.py +359 -0
  173. openinterp/errata_checks.py +129 -0
  174. openinterp/evidence.py +462 -0
  175. openinterp/evidence_branch.py +119 -0
  176. openinterp/evidence_lenses.py +75 -0
  177. openinterp/experiment_loop.py +357 -0
  178. openinterp/explain.py +252 -0
  179. openinterp/half_precision.py +79 -0
  180. openinterp/harness/__init__.py +37 -0
  181. openinterp/harness/ane/__init__.py +9 -0
  182. openinterp/harness/ane/backend.py +324 -0
  183. openinterp/harness/ane/cache.py +276 -0
  184. openinterp/harness/ane/chunks.py +163 -0
  185. openinterp/harness/ane/convert.py +116 -0
  186. openinterp/harness/ane/forward.py +258 -0
  187. openinterp/harness/ane/qualified.py +40 -0
  188. openinterp/harness/annotations.py +329 -0
  189. openinterp/harness/attribution.py +96 -0
  190. openinterp/harness/causal.py +264 -0
  191. openinterp/harness/confounds.py +184 -0
  192. openinterp/harness/covariance.py +301 -0
  193. openinterp/harness/crossref.py +195 -0
  194. openinterp/harness/cuda_devices.py +88 -0
  195. openinterp/harness/dead.py +124 -0
  196. openinterp/harness/devices.py +401 -0
  197. openinterp/harness/dictionaries.py +496 -0
  198. openinterp/harness/dreaming.py +116 -0
  199. openinterp/harness/environment.py +64 -0
  200. openinterp/harness/explanation_scores.py +371 -0
  201. openinterp/harness/features.py +276 -0
  202. openinterp/harness/first_position.py +137 -0
  203. openinterp/harness/heads.py +226 -0
  204. openinterp/harness/hooks.py +499 -0
  205. openinterp/harness/input_attribution.py +367 -0
  206. openinterp/harness/jlens.py +183 -0
  207. openinterp/harness/layer_cache.py +301 -0
  208. openinterp/harness/layer_major.py +274 -0
  209. openinterp/harness/lookahead.py +93 -0
  210. openinterp/harness/manifold.py +200 -0
  211. openinterp/harness/memory.py +471 -0
  212. openinterp/harness/moe.py +134 -0
  213. openinterp/harness/patcher.py +322 -0
  214. openinterp/harness/patchscope.py +177 -0
  215. openinterp/harness/probing.py +383 -0
  216. openinterp/harness/scan.py +462 -0
  217. openinterp/harness/scan_backend.py +201 -0
  218. openinterp/harness/sister_passes.py +463 -0
  219. openinterp/harness/sisters.py +363 -0
  220. openinterp/harness/sites.py +101 -0
  221. openinterp/harness/specificity.py +107 -0
  222. openinterp/harness/steering.py +159 -0
  223. openinterp/harness/superpositions.py +116 -0
  224. openinterp/harness/units.py +203 -0
  225. openinterp/harness/wiring.py +432 -0
  226. openinterp/harness/wiring_activations.py +285 -0
  227. openinterp/langid.py +73 -0
  228. openinterp/names.py +129 -0
  229. openinterp/neuronpedia_baseline.py +88 -0
  230. openinterp/neuronpedia_review.py +148 -0
  231. openinterp/planner.py +466 -0
  232. openinterp/planner_effects.py +89 -0
  233. openinterp/planner_heads.py +92 -0
  234. openinterp/planner_models.py +174 -0
  235. openinterp/planner_sisters.py +122 -0
  236. openinterp/planner_states.py +57 -0
  237. openinterp/planner_superpositions.py +114 -0
  238. openinterp/pole_effects.py +102 -0
  239. openinterp/prior_art.py +143 -0
  240. openinterp/reproduce.py +450 -0
  241. openinterp/revision.py +45 -0
  242. openinterp/runner_cache.py +35 -0
  243. openinterp/score_controls.py +169 -0
  244. openinterp/sensitive.py +243 -0
  245. openinterp/show.py +236 -0
  246. openinterp/signing.py +209 -0
  247. openinterp/sisters.py +476 -0
  248. openinterp/superposition_finders.py +225 -0
  249. openinterp/sync/__init__.py +5 -0
  250. openinterp/sync/r2.py +280 -0
  251. openinterp/syntax.py +399 -0
  252. openinterp/takedown_checks.py +76 -0
  253. openinterp/toxicity.py +158 -0
  254. openinterp/verdict_regression.py +79 -0
  255. openinterp/wiring_backfill.py +170 -0
  256. openinterp/wordnet.py +362 -0
@@ -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}")
@@ -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}")