interp-engine 1.14.0__tar.gz → 1.14.2__tar.gz

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 (225) hide show
  1. {interp_engine-1.14.0 → interp_engine-1.14.2}/PKG-INFO +1 -1
  2. {interp_engine-1.14.0 → interp_engine-1.14.2}/docs/AGENT_INTEGRATION.md +7 -5
  3. {interp_engine-1.14.0 → interp_engine-1.14.2}/docs/ARCHITECTURE_QUIRKS.md +1 -1
  4. {interp_engine-1.14.0 → interp_engine-1.14.2}/docs/SUPPORTED_POINTS.md +1 -1
  5. {interp_engine-1.14.0 → interp_engine-1.14.2}/docs/USAGE.md +11 -11
  6. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/capture.py +2 -2
  7. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/hooks.py +1 -1
  8. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/points.py +12 -9
  9. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/protocol.py +11 -11
  10. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/tokenize.py +4 -3
  11. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/_tp.py +1 -3
  12. {interp_engine-1.14.0 → interp_engine-1.14.2}/pyproject.toml +1 -1
  13. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_points_registry.py +7 -0
  14. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_qkv_layout.py +1 -1
  15. interp_engine-1.14.2/tests/test_tool_call_spans.py +87 -0
  16. {interp_engine-1.14.0 → interp_engine-1.14.2}/.gitignore +0 -0
  17. {interp_engine-1.14.0 → interp_engine-1.14.2}/LICENSE +0 -0
  18. {interp_engine-1.14.0 → interp_engine-1.14.2}/README.md +0 -0
  19. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/README.md +0 -0
  20. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/__init__.py +0 -0
  21. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/bench_spec.py +0 -0
  22. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/cells.py +0 -0
  23. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/probe.py +0 -0
  24. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/probe_lens_stream.py +0 -0
  25. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/publish.py +0 -0
  26. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/report_bench.py +0 -0
  27. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
  28. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
  29. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/deepseek-v4-flash-0731__vllm-static.json +0 -0
  30. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
  31. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/gemma-2-2b__eager.json +0 -0
  32. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
  33. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/gemma-2-2b__vllm-static.json +0 -0
  34. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
  35. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
  36. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
  37. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/llama-3.1-8b__vllm-static.json +0 -0
  38. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
  39. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/qwen3-4b__eager.json +0 -0
  40. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
  41. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/qwen3-4b__vllm-static.json +0 -0
  42. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/qwen3-4b__vllm.json +0 -0
  43. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
  44. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
  45. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/qwen3.8-27b__vllm-static.json +0 -0
  46. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
  47. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/results-latest.md +0 -0
  48. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/run_all.sh +0 -0
  49. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/run_bench.py +0 -0
  50. {interp_engine-1.14.0 → interp_engine-1.14.2}/benchmarks/workloads.py +0 -0
  51. {interp_engine-1.14.0 → interp_engine-1.14.2}/docs/COMPATIBILITY.md +0 -0
  52. {interp_engine-1.14.0 → interp_engine-1.14.2}/docs/ENGINE_HOOK_MAPPINGS.md +0 -0
  53. {interp_engine-1.14.0 → interp_engine-1.14.2}/docs/GRADIENTS.md +0 -0
  54. {interp_engine-1.14.0 → interp_engine-1.14.2}/docs/INTERNALS.md +0 -0
  55. {interp_engine-1.14.0 → interp_engine-1.14.2}/docs/PERFORMANCE.md +0 -0
  56. {interp_engine-1.14.0 → interp_engine-1.14.2}/docs/PORTING.md +0 -0
  57. {interp_engine-1.14.0 → interp_engine-1.14.2}/docs/README.md +0 -0
  58. {interp_engine-1.14.0 → interp_engine-1.14.2}/docs/VALIDATING_A_NEW_MODEL.md +0 -0
  59. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/__init__.py +0 -0
  60. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/_conformance.py +0 -0
  61. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/_loop.py +0 -0
  62. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/address.py +0 -0
  63. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/api.py +0 -0
  64. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/arch.py +0 -0
  65. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/attn_config.py +0 -0
  66. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/attn_scores.py +0 -0
  67. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/autograd_support.py +0 -0
  68. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/chat_compose.py +0 -0
  69. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/chat_conventions.py +0 -0
  70. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/chat_formatters.py +0 -0
  71. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/cuda_preflight.py +0 -0
  72. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/describe.py +0 -0
  73. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/directions.py +0 -0
  74. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/dispatch.py +0 -0
  75. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/facts.py +0 -0
  76. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/lens.py +0 -0
  77. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/lens_stream.py +0 -0
  78. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/lens_topk.py +0 -0
  79. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/load.py +0 -0
  80. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/mappers.py +0 -0
  81. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/memory.py +0 -0
  82. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/model.py +0 -0
  83. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/moe_routing.py +0 -0
  84. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/notebook_stdout.py +0 -0
  85. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/oracle.py +0 -0
  86. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/residual_basis.py +0 -0
  87. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/sampling.py +0 -0
  88. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/select.py +0 -0
  89. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/server/__init__.py +0 -0
  90. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/server/__main__.py +0 -0
  91. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/server/app.py +0 -0
  92. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/server/config.py +0 -0
  93. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/server/lens.py +0 -0
  94. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/server/runner.py +0 -0
  95. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/server/schemas.py +0 -0
  96. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/steer.py +0 -0
  97. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/steer_specs.py +0 -0
  98. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/sync.py +0 -0
  99. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_backend.py +0 -0
  100. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/__init__.py +0 -0
  101. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/_demux.py +0 -0
  102. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/_hooks.py +0 -0
  103. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/_payload.py +0 -0
  104. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/_tree.py +0 -0
  105. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/attn.py +0 -0
  106. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/capture.py +0 -0
  107. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/graphs.py +0 -0
  108. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/lens/__init__.py +0 -0
  109. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/lens/intervene.py +0 -0
  110. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/lens/readout.py +0 -0
  111. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/lens/unembed.py +0 -0
  112. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/mhc.py +0 -0
  113. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/native.py +0 -0
  114. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/project.py +0 -0
  115. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/requests.py +0 -0
  116. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/static.py +0 -0
  117. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/static_program.py +0 -0
  118. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_capture/steering.py +0 -0
  119. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_plugin.py +0 -0
  120. {interp_engine-1.14.0 → interp_engine-1.14.2}/interp_engine/vllm_seq_fit.py +0 -0
  121. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/conftest.py +0 -0
  122. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/harness.py +0 -0
  123. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/model_expectations.yaml +0 -0
  124. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/synthetic_families.py +0 -0
  125. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_address.py +0 -0
  126. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_api_fixtures.py +0 -0
  127. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_api_generated.py +0 -0
  128. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_attn_config_tripwire.py +0 -0
  129. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_attn_probs_indexing.py +0 -0
  130. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_attn_scores.py +0 -0
  131. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_attn_z_gqa.py +0 -0
  132. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_autograd_support.py +0 -0
  133. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_bench_workloads.py +0 -0
  134. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_capability_refusals.py +0 -0
  135. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_capture_addressing.py +0 -0
  136. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_chat_compose.py +0 -0
  137. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_chat_formatters.py +0 -0
  138. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_chat_templates.py +0 -0
  139. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_core.py +0 -0
  140. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_cuda_preflight.py +0 -0
  141. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_describe.py +0 -0
  142. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_directions.py +0 -0
  143. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_doc_code_fences.py +0 -0
  144. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_eager_autograd.py +0 -0
  145. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_facts.py +0 -0
  146. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_family_points.py +0 -0
  147. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_gated_attn_out.py +0 -0
  148. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_gpu_sizer.py +0 -0
  149. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_head_contributions.py +0 -0
  150. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_hook_call_conventions.py +0 -0
  151. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_injected_system_spans.py +0 -0
  152. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_layer_kinds.py +0 -0
  153. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_lens_stream.py +0 -0
  154. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_lens_stream_pairing.py +0 -0
  155. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_load.py +0 -0
  156. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_logit_transform.py +0 -0
  157. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_mappers.py +0 -0
  158. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_memory.py +0 -0
  159. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_mlp_internals.py +0 -0
  160. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_model_expectations.py +0 -0
  161. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_moe.py +0 -0
  162. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_multigpu.py +0 -0
  163. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_multimodal_arch.py +0 -0
  164. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_new_models_gpu.py +0 -0
  165. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_no_chat_template.py +0 -0
  166. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_normalized_hook.py +0 -0
  167. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_notebook_stdout.py +0 -0
  168. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_oracle.py +0 -0
  169. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_packaging.py +0 -0
  170. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_padded_positions.py +0 -0
  171. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_parity_gpt2.py +0 -0
  172. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_per_layer_attn_dims.py +0 -0
  173. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_point_capability.py +0 -0
  174. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_protocol.py +0 -0
  175. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_published_benchmarks.py +0 -0
  176. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_qk_norm.py +0 -0
  177. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_reasoning_spans.py +0 -0
  178. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_release.py +0 -0
  179. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_resid_mid.py +0 -0
  180. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_residual_basis.py +0 -0
  181. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_sample_from_embeds.py +0 -0
  182. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_sampling.py +0 -0
  183. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_sandwich_norms.py +0 -0
  184. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_select.py +0 -0
  185. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_server.py +0 -0
  186. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_sliding_window_attn.py +0 -0
  187. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_small_models_gpu.py +0 -0
  188. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_static_decode_writes_gpu.py +0 -0
  189. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_static_dsv4_gpu.py +0 -0
  190. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_static_parity_gpu.py +0 -0
  191. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_static_program.py +0 -0
  192. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_static_program_gpu.py +0 -0
  193. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_static_set.py +0 -0
  194. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_static_warmup.py +0 -0
  195. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_steer_context.py +0 -0
  196. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_steer_math_parity.py +0 -0
  197. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_steer_ops_per_site_gpu.py +0 -0
  198. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_steer_reaches_generate.py +0 -0
  199. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_steer_scope.py +0 -0
  200. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_steer_specs.py +0 -0
  201. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_steer_stream_stack.py +0 -0
  202. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_sync_loop.py +0 -0
  203. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_sync_parity.py +0 -0
  204. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_unified_free_functions.py +0 -0
  205. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_unresolved_families.py +0 -0
  206. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_userless_prefix_spans.py +0 -0
  207. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_batch_axis.py +0 -0
  208. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_capture_gpu.py +0 -0
  209. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_capture_scales.py +0 -0
  210. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_engine_loop.py +0 -0
  211. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_graph_path.py +0 -0
  212. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_graphs_on_gpu.py +0 -0
  213. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_hook_availability.py +0 -0
  214. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_hyper_connections.py +0 -0
  215. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_kv_isolation.py +0 -0
  216. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_new_points.py +0 -0
  217. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_only_families.py +0 -0
  218. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_padded_gpu.py +0 -0
  219. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_plugin.py +0 -0
  220. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_seq_fit.py +0 -0
  221. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_transformers_backend_gpu.py +0 -0
  222. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vllm_wire_grammar.py +0 -0
  223. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_vocabulary_boundary.py +0 -0
  224. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_worker_lens_capture_readout.py +0 -0
  225. {interp_engine-1.14.0 → interp_engine-1.14.2}/tests/test_worker_lens_readout.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: interp-engine
3
- Version: 1.14.0
3
+ Version: 1.14.2
4
4
  Summary: A fast, standardized, and easy to use interpretability engine.
5
5
  Project-URL: Homepage, https://github.com/decoderesearch/interp-engine
6
6
  Project-URL: Repository, https://github.com/decoderesearch/interp-engine
@@ -134,7 +134,7 @@ engine_kwargs = {"worker_extension_cls": WORKER_EXTENSION_CLS, **capture_engine_
134
134
 
135
135
  The decision rule, in order:
136
136
 
137
- 1. Do you need eager-only points (MLP internals, MoE selection), gradients, or module/weight access?
137
+ 1. Do you need eager-only points (MLP input branches, MoE selection), gradients, or module/weight access?
138
138
  → **eager**, `load_model(..., backend="eager")`. Neither tier of vLLM serves these; see [the
139
139
  vocabulary table](#the-point-vocabulary). Attention is *not* on that list any more —
140
140
  `capture_attention` serves it on both.
@@ -189,14 +189,16 @@ Canonical names, with the layer after a dot: `resid_post.10`. Extra coordinates
189
189
  | `attn_out` | attention's raw output | both |
190
190
  | `mlp_out_post`, `attn_out_post` | that sublayer's **residual contribution** — differs from the raw output only on post-norm architectures | both |
191
191
  | `z` | per-head attention output, `n_heads * head_dim` wide (**not** `d_model` on every family) | both |
192
- | `value`, `attn_probs`, `attn_scores` | per-head values and the attention matrix | both, but through `capture_attention` rather than as points on vLLM |
193
- | `mlp_act`, `mlp_pre`, `mlp_pre_linear` | MLP internals, `d_mlp` wide | **eager only** |
192
+ | `value` | per-head values, the value projection's output on most families | both (head-sharded; gathered across TP ranks) |
193
+ | `attn_probs`, `attn_scores` | the attention matrix | both, but through `capture_attention` rather than as points on vLLM |
194
+ | `mlp_act` | post-activation neurons, the down projection's input, `d_mlp` wide | both (neuron-sharded; gathered across TP ranks) |
195
+ | `mlp_pre`, `mlp_pre_linear` | the MLP's input branches, `d_mlp` wide | **eager only** |
194
196
  | `router_logits` | MoE routing scores, every expert | both |
195
197
  | `expert_weights`, `expert_indices` | the top-k the router selected, and its weights | **eager only** |
196
198
  | the QK-norm points | inside the attention module | both (head-sharded; gathered across TP ranks) |
197
199
 
198
- Eager-only is not an omission: vLLM's fused MLP and MoE kernels compute those tensors inline, so
199
- there is no module boundary to hook. This table is the working subset;
200
+ Eager-only is not an omission: vLLM fuses `gate_proj` and `up_proj` into one `gate_up_proj`, and its
201
+ MoE kernel forms the selection inline, so there is no module boundary to hook. This table is the working subset;
200
202
  [SUPPORTED_POINTS.md](SUPPORTED_POINTS.md) is all 34 points with the per-backend verdict on each,
201
203
  and it is checked against the registry rather than maintained by hand.
202
204
 
@@ -311,7 +311,7 @@ must split", on a family whose `fused_qkv` is false — so `value` no longer ask
311
311
  **Reading a norm costs a rank, and the point does not pay it.** A norm over `head_dim` can only be
312
312
  given the per-head view, so both engines hand `v_norm` a `(…, n_kv_heads, head_dim)` tensor where a
313
313
  `v_proj` would have produced the flat one — and `value` would then be 4-D on this family and 3-D on the
314
- next, against a `Width.HEADS` declaration and `capture`'s `[batch, seq, width]`. Flattened back
314
+ next, against a `Width.KV_HEADS` declaration and `capture`'s `[batch, seq, width]`. Flattened back
315
315
  on both sides (`hooks.flat_per_head`, `vllm_capture._hooks.flat_value`), including on the way *into* a
316
316
  steer, so a vector measured on a capture of `value` is the shape the write expects; the module gets its
317
317
  own rank back before the attention reshapes it. Eagerly the head count is checked rather than assumed,
@@ -16,7 +16,7 @@ nnsight and nnterp.
16
16
  | [`attn_in`][attn_in] | `d_model` | ✅ | ✅ | |
17
17
  | [`q_norm_in`][q_norm_in] / [`q_norm_out`][q_norm_out] | `n_heads * head_dim` | ✅ | ✅ | head-sharded; gathered across TP ranks at collect |
18
18
  | [`k_norm_in`][k_norm_in] / [`k_norm_out`][k_norm_out] | `n_kv_heads * head_dim` | ✅ | ✅ | head-sharded; gathered across TP ranks at collect |
19
- | [`value`][value] | `n_heads * head_dim` | ✅ | ✅ | head-sharded; gathered across TP ranks at collect |
19
+ | [`value`][value] | `n_kv_heads * head_dim` | ✅ | ✅ | head-sharded; gathered across TP ranks at collect |
20
20
  | [`attn_scores`][attn_scores] | `n_heads * query * key` | ✅ | ♻️ | no module boundary holds the pre-softmax matrix on **either** backend; vLLM rebuilds it from captured post-RoPE q/k |
21
21
  | [`attn_probs`][attn_probs] | `n_heads * query * key` | ✅ | ♻️ | fused paged attention never materializes the probabilities; same recompute |
22
22
  | [`z`][z] | `n_heads * head_dim` | ✅ | ✅ | head-sharded; gathered across TP ranks at collect |
@@ -148,8 +148,8 @@ Three things to know about what comes back:
148
148
  one up, or `format_address(key)` to print one. (The eager `Cache` from `capture` is the
149
149
  exception — it accepts either form on lookup.)
150
150
  - **Shape is `[n_prompt_tokens, width]`, on CPU, with no batch dimension** — on either backend.
151
- `width` is `d_model` for the residual and MLP points, and `n_heads * head_dim` for `z`, which is
152
- *not* `d_model` on every family.
151
+ `width` is `d_model` for the residual and sublayer-boundary points, `d_mlp` for `mlp_act`, and
152
+ `n_heads * head_dim` for `z`, which is *not* `d_model` on every family.
153
153
  - **Rows are prompt positions in order**, so `acts[address][3]` is the activation at token 3.
154
154
 
155
155
  ## Naming a point
@@ -167,16 +167,16 @@ Points every backend serves: `resid_pre`, `resid_mid`, `resid_post`, `mlp_in`, `
167
167
  `attn_out`, `mlp_out_post`, `attn_out_post`, `z`. [SUPPORTED_POINTS.md](SUPPORTED_POINTS.md) is the
168
168
  full list, with the width and the per-backend verdict for each.
169
169
 
170
- Eager-only, because they are formed inside a fused kernel vLLM never unfolds: the MLP internals
171
- (`mlp_act`, `mlp_pre`, `mlp_pre_linear`) and the MoE *selection* (`expert_weights`,
172
- `expert_indices`) — its `router_logits` are served on both, since the gate produces them before any
173
- kernel takes over.
170
+ Eager-only, because they are formed inside a fused kernel vLLM never unfolds: the MLP input branches
171
+ (`mlp_pre`, `mlp_pre_linear`, which vLLM fuses into one `gate_up_proj`) and the MoE *selection*
172
+ (`expert_weights`, `expert_indices`). The neuron basis `mlp_act` is the down projection's input, and
173
+ `router_logits` comes from the gate before any kernel takes over, so both are served on both.
174
174
 
175
- The attention trio (`attn_scores`, `attn_probs`, `value`) is served on both, but not as a point on
176
- vLLM: no module boundary holds a score matrix the paged kernel never forms, so both backends
177
- reconstruct it. Ask for it with `capture_attention`, which returns the same
178
- `{layer: {"scores", "probs", "value"}}` either way — see [Without an event
179
- loop](#without-an-event-loop).
175
+ The attention matrix (`attn_scores`, `attn_probs`) is served on both, but not as a point on vLLM:
176
+ no module boundary holds a score matrix the paged kernel never forms, so both backends reconstruct
177
+ it. Ask for it with `capture_attention`, which returns the same `{layer: {"scores", "probs",
178
+ "value"}}` either way — see [Without an event loop](#without-an-event-loop). The raw `value` point
179
+ is an ordinary capture point on both.
180
180
 
181
181
  The `*_post` pair deserves attention: `mlp_out` is the raw module output and `mlp_out_post` is that
182
182
  sublayer's *contribution to the residual stream*. They differ only on post-norm architectures
@@ -554,7 +554,7 @@ def _capture_eager(
554
554
  raw = cache.tensors[key]
555
555
  if model.arch.quirks.fused_qkv:
556
556
  # The value alone, not the `[q | k | v]` slab the fused projection emits. This point
557
- # declares `Width.HEADS` and both vLLM backends return exactly that, so leaving the slab
557
+ # declares `Width.KV_HEADS` and both vLLM backends return exactly that, so leaving the slab
558
558
  # here made the reference three times too wide on gpt2, GPT-NeoX and Falcon -- and the
559
559
  # engine every other one is scored against was the one disagreeing.
560
560
  raw = split_fused_qkv(model, raw)["v"]
@@ -699,7 +699,7 @@ def per_head_value(model: EagerModel, cache: Cache, layer: int) -> torch.Tensor:
699
699
  point, is what satisfies ``probs @ value == z``.
700
700
 
701
701
  A fused qkv is already split by the time the point lands in the cache, so there is nothing to
702
- unpack here: the raw point is the value at ``Width.HEADS`` on every family.
702
+ unpack here: the raw point is the value at ``Width.KV_HEADS`` on every family.
703
703
  """
704
704
  raw = cache.get("value", layer) # [batch, seq, n_kv_heads * v_head_dim]
705
705
  batch, seq, _ = raw.shape
@@ -54,7 +54,7 @@ def flat_per_head(tensor: torch.Tensor, *, heads: int) -> tuple[torch.Tensor, to
54
54
  The module `value` resolves to is a *norm* wherever the family has one, and a norm over `head_dim`
55
55
  has to be given the per-head view: Gemma-4 runs `v_norm` on `v_proj(h).view(..., n_kv, head_dim)`
56
56
  and vLLM runs the same norm on `v.unflatten(-1, (n_kv, head_dim))`. So the point arrives per head
57
- on one family and flat on the next, while `Width.HEADS` and `capture` both promise one
57
+ on one family and flat on the next, while `Width.KV_HEADS` and `capture` both promise one
58
58
  rank -- and TransformerLens' `hook_v` is the per-head spelling of this point, which is a
59
59
  distinction the mapping table would lose if ours moved family to family.
60
60
 
@@ -64,11 +64,13 @@ class Width(Enum):
64
64
  """What the last axis of the captured tensor counts -- the axis a shard would narrow.
65
65
 
66
66
  This is the tensor-parallel question, which is why it is an enum rather than a
67
- ``d_model: bool``. ``HEADS``, ``NEURONS`` and ``VOCAB`` are exactly the axes vLLM shards, so
68
- under TP rank 0 holds a slice of them -- and for the first two there is no width to check it
69
- against either (``n_heads * head_dim`` coincides with ``hidden_size`` on Llama and not on
70
- Gemma-3, so a width check would pass on one and fail on the other for reasons unrelated to
71
- sharding). :func:`tp_sharded` is that set.
67
+ ``d_model: bool``. ``HEADS``, ``KV_HEADS``, ``NEURONS`` and ``VOCAB`` are exactly the axes vLLM
68
+ shards, so under TP rank 0 holds a slice of them -- and for the first three there is no width to
69
+ check it against either (``n_heads * head_dim`` coincides with ``hidden_size`` on Llama and not
70
+ on Gemma-3, so a width check would pass on one and fail on the other for reasons unrelated to
71
+ sharding). :func:`tp_sharded` is that set. ``KV_HEADS`` is its own member because a GQA model
72
+ has fewer KV heads than query heads, and because vLLM replicates KV heads, not shards them,
73
+ when there are fewer of them than ranks.
72
74
 
73
75
  The rest reach a hook at full width, for three different reasons, which is why "is it
74
76
  ``D_MODEL``" is *not* the same question: ``D_MODEL`` is all-reduced before the hook sees it,
@@ -81,6 +83,7 @@ class Width(Enum):
81
83
 
82
84
  D_MODEL = "d_model"
83
85
  HEADS = "n_heads * head_dim"
86
+ KV_HEADS = "n_kv_heads * head_dim"
84
87
  NEURONS = "d_mlp"
85
88
  ROUTING = "experts"
86
89
  SCORES = "n_heads * query * key"
@@ -95,7 +98,7 @@ class Width(Enum):
95
98
  @classmethod
96
99
  def sharded(cls) -> frozenset[Width]:
97
100
  """The widths a tensor-parallel rank holds only a slice of. See the class docstring."""
98
- return frozenset({cls.HEADS, cls.NEURONS, cls.VOCAB})
101
+ return frozenset({cls.HEADS, cls.KV_HEADS, cls.NEURONS, cls.VOCAB})
99
102
 
100
103
 
101
104
  class VllmSupport(Enum):
@@ -151,9 +154,9 @@ POINTS: tuple[PointSpec, ...] = (
151
154
  _p("attn_in", _L, Width.D_MODEL, _HOOKS),
152
155
  _p("q_norm_in", _L, Width.HEADS, _HOOKS),
153
156
  _p("q_norm_out", _L, Width.HEADS, _HOOKS),
154
- _p("k_norm_in", _L, Width.HEADS, _HOOKS),
155
- _p("k_norm_out", _L, Width.HEADS, _HOOKS),
156
- _p("value", _L, Width.HEADS, _HOOKS),
157
+ _p("k_norm_in", _L, Width.KV_HEADS, _HOOKS),
158
+ _p("k_norm_out", _L, Width.KV_HEADS, _HOOKS),
159
+ _p("value", _L, Width.KV_HEADS, _HOOKS),
157
160
  _p(
158
161
  "attn_scores",
159
162
  _L,
@@ -20,8 +20,8 @@ both backends, and are what a sync caller should use when the backend is not kno
20
20
 
21
21
  Deliberately NOT in the protocol:
22
22
 
23
- - **Per-head points through ``capture``** (``value``, ``attn_probs``). No backend holds these
24
- at a module boundary, so each reconstructs them, and the reconstruction is
23
+ - **The attention matrix through ``capture``** (``attn_scores``, ``attn_probs``). No backend
24
+ holds these at a module boundary, so each reconstructs them, and the reconstruction is
25
25
  ``capture_attention`` -- which *is* in the protocol. What is not is asking for them as
26
26
  ordinary capture points. Gate on ``refuses(point, layer)`` rather than on the backend name.
27
27
  - **vLLM ``SamplingParams`` over embeddings** (``VLLMModel.generate_from_embeds``, deprecated).
@@ -398,17 +398,17 @@ class InterpModel(Protocol):
398
398
  token, in order, with no batch dimension on either backend. With ``rows``, only those
399
399
  positions come back, in that order (``[len(rows), width]``); on vLLM the worker drops
400
400
  the others before they cross the process boundary. ``width`` is
401
- :attr:`d_model` for the residual and MLP points and ``n_heads * head_dim`` for
402
- ``z``. Requests may be ``Address``es, their canonical string forms, or the
403
- ``(name, layer)`` tuples this used to take; the keys coming back are always
404
- ``Address``es, since that is the only form that can carry every coordinate.
401
+ :attr:`d_model` for the residual and sublayer-boundary points, ``d_mlp`` for
402
+ ``mlp_act``, and ``n_heads * head_dim`` for ``z``. Requests may be ``Address``es, their
403
+ canonical string forms, or the ``(name, layer)`` tuples this used to take; the keys coming
404
+ back are always ``Address``es, since that is the only form that can carry every coordinate.
405
405
 
406
406
  ``points`` names are the canonical ones (``resid_pre``, ``resid_mid``, ``resid_post``,
407
- ``mlp_in``, ``mlp_out``, ``attn_out``, ``mlp_out_post``, ``attn_out_post``, ``z``); ``value``,
408
- ``attn_probs``, ``attn_scores``, the QK-norm points, the MLP-internal points (``mlp_act``,
409
- ``mlp_pre``, ``mlp_pre_linear``) and the MoE routing points (``router_logits``,
410
- ``expert_weights``, ``expert_indices``) are eager-only, see the module
411
- docstring. The ``*_post`` pair is the
407
+ ``mlp_in``, ``mlp_out``, ``attn_out``, ``mlp_out_post``, ``attn_out_post``, ``z``,
408
+ ``value``, ``mlp_act``, ``router_logits``, the QK-norm points); ``mlp_pre``,
409
+ ``mlp_pre_linear``, ``expert_weights`` and ``expert_indices`` are eager-only, and
410
+ ``attn_probs`` / ``attn_scores`` come from ``capture_attention``, see the module
411
+ docstring. :func:`interp_engine.points.eager_only` is the full list. The ``*_post`` pair is the
412
412
  sublayer's residual *contribution*, which differs from the raw output only on post-norm
413
413
  architectures (Gemma-2/3/4, OLMo-2/3) and aliases it everywhere else. With ``steering_spec`` (a ``SteeringSpec``, or a list for several points) the
414
414
  activations are captured from the *steered* forward, not a separate one.
@@ -589,8 +589,8 @@ class Tokenize:
589
589
  ) -> tuple[int, int]:
590
590
  """Return ``(header_len, footer_len)`` for message ``k``'s block ``full_ids[start:end]``.
591
591
 
592
- Renders message ``k`` with EMPTY content: the tokens it still contributes are exactly
593
- the structural wrapper (header + footer). Aligning that wrapper against the real block by
592
+ Renders message ``k`` with EMPTY content and no tool calls: the tokens it still
593
+ contributes are exactly the structural wrapper (header + footer). Aligning that wrapper against the real block by
594
594
  common prefix / suffix locates the wrapper, and everything between is the content.
595
595
 
596
596
  The wrapper is rendered CLOSED even for a prefill (a final message held open by
@@ -614,7 +614,8 @@ class Tokenize:
614
614
  block = full_ids[start:end]
615
615
  block_len = end - start
616
616
  emptied = [dict(m) for m in messages]
617
- emptied[k] = {**emptied[k], "content": ""}
617
+ # A tool call is text the model wrote, so it goes with the content, not the wrapper.
618
+ emptied[k] = {key: v for key, v in emptied[k].items() if key != "tool_calls"} | {"content": ""}
618
619
  try:
619
620
  if self.formatter is not None:
620
621
  ids_empty = self._encode_rendered(
@@ -29,8 +29,6 @@ from interp_engine.address import parse_address
29
29
  from interp_engine.points import Width, point_spec, tp_sharded
30
30
  from interp_engine.vllm_capture._tree import _attn_module, _get_layers, _worker_tp_world_size
31
31
 
32
- #: Points whose heads are the *KV* heads, and so may be replicated across ranks rather than sharded.
33
- _KV_HEAD_POINTS = frozenset({"k_norm_in", "k_norm_out", "value"})
34
32
  #: Attention payload roles by which head count they carry.
35
33
  _ATTN_ROLE_KV = frozenset({"k", "v"})
36
34
  _SHARDED_POINTS = tp_sharded()
@@ -138,7 +136,7 @@ def gather_capture(model: torch.nn.Module, key: str, tensor: torch.Tensor) -> to
138
136
  if counts is None:
139
137
  return _all_gather(tensor, -1)
140
138
  n_heads, n_kv, total_kv, head_dim = counts
141
- if address.name in _KV_HEAD_POINTS:
139
+ if spec is not None and spec.width is Width.KV_HEADS:
142
140
  return gather_heads(tensor, per_rank=n_kv, total=total_kv, head_dim=head_dim)
143
141
  return gather_heads(tensor, per_rank=n_heads, total=None, head_dim=head_dim)
144
142
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "interp-engine"
3
- version = "1.14.0"
3
+ version = "1.14.2"
4
4
  description = "A fast, standardized, and easy to use interpretability engine."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -251,6 +251,13 @@ def test_the_sharded_set_is_the_narrowed_axes_and_not_just_the_non_d_model_ones(
251
251
  assert not ({"router_logits", "resid_post", "attn_in", "resid_streams"} & sharded)
252
252
 
253
253
 
254
+ def test_the_kv_head_points_are_declared_kv_head_wide():
255
+ """On a GQA model these are ``n_kv_heads * head_dim``, and the TP gather reads this width to
256
+ pick the KV head count and drop replicated KV heads."""
257
+ kv = {p.name for p in points.POINTS if p.width is points.Width.KV_HEADS}
258
+ assert kv == {"k_norm_in", "k_norm_out", "value"}
259
+
260
+
254
261
  def test_the_mapping_doc_marks_exactly_the_eager_only_points():
255
262
  """The footnote markers in the docs table, checked against the table in code.
256
263
 
@@ -136,7 +136,7 @@ def test_per_head_value_agrees_with_the_split(spec: ModelSpec, expected: QKVLayo
136
136
 
137
137
  @pytest.mark.parametrize(("spec", "expected"), FUSED_MODELS)
138
138
  def test_the_raw_value_point_is_the_value_and_not_the_whole_fused_slab(spec: ModelSpec, expected: QKVLayout):
139
- """`value` declares ``Width.HEADS``, so a fused family has to be split before the point is served.
139
+ """`value` declares ``Width.KV_HEADS``, so a fused family has to be split before the point is served.
140
140
 
141
141
  It was not: the raw point handed back ``[q | k | v]``, three times too wide on gpt2 -- and on the
142
142
  one engine every other one is scored against. Both vLLM backends already returned the value alone,
@@ -0,0 +1,87 @@
1
+ """Spans for a chat with tool calls: a call is what the model wrote, so it is the turn's content.
2
+
3
+ Qwen3.6 renders an assistant turn's ``tool_calls`` after its text, inside the turn. The header
4
+ and footer are found by rendering the turn with no content, so a call that stays in that render
5
+ became part of the header: the whole call, up to the turn-end token.
6
+
7
+ Tokenizer-only: nothing here runs a forward.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from functools import cache
13
+
14
+ import pytest
15
+ from transformers import AutoTokenizer
16
+
17
+ from interp_engine.tokenize import Tokenize
18
+
19
+ QWEN36 = "Qwen/Qwen3.6-27B"
20
+
21
+
22
+ def _call(name: str, arguments: dict[str, str], id_: str) -> dict:
23
+ return {"type": "function", "id": id_, "function": {"name": name, "arguments": arguments}}
24
+
25
+
26
+ CHAT = [
27
+ {"role": "user", "content": "Find the weather in Paris and in Rome."},
28
+ {"role": "assistant", "content": "", "tool_calls": [_call("get_weather", {"city": "Paris"}, "call_1")]},
29
+ {"role": "tool", "content": "Sunny", "tool_call_id": "call_1"},
30
+ {
31
+ "role": "assistant",
32
+ "content": "One more.",
33
+ "tool_calls": [_call("get_weather", {"city": "Rome"}, "call_2"), _call("get_time", {"city": "Rome"}, "c3")],
34
+ },
35
+ {"role": "tool", "content": "Rain", "tool_call_id": "call_2"},
36
+ {"role": "tool", "content": "Noon", "tool_call_id": "c3"},
37
+ {"role": "assistant", "content": "Paris is sunny; Rome has rain."},
38
+ ]
39
+
40
+
41
+ @cache
42
+ def _tok() -> Tokenize:
43
+ try:
44
+ return Tokenize(AutoTokenizer.from_pretrained(QWEN36))
45
+ except Exception as exc: # noqa: BLE001 - offline / uncached
46
+ pytest.skip(f"could not load tokenizer {QWEN36}: {exc}")
47
+
48
+
49
+ def _spans(prefill: bool = False):
50
+ options = {"add_generation_prompt": not prefill, "continue_final_message": prefill}
51
+ tok = _tok()
52
+ spans = tok.message_spans(CHAT, enable_thinking=False, **options)
53
+ ids = tok.apply_chat_template(CHAT, tokenize=True, enable_thinking=False, **options)
54
+ assert [s.token_id for s in spans] == list(ids)
55
+ return spans
56
+
57
+
58
+ def _text(spans, index: int, section: str) -> str:
59
+ return "".join(s.token_str for s in spans if s.message_index == index and s.section == section)
60
+
61
+
62
+ def test_a_tool_call_is_content_not_header():
63
+ spans = _spans()
64
+ for index in (1, 3):
65
+ header, content, footer = (_text(spans, index, s) for s in ("header", "content", "footer"))
66
+ assert "<tool_call>" not in header
67
+ assert "<function=get_weather>" in content
68
+ assert content.rstrip().endswith("</tool_call>")
69
+ assert footer == "<|im_end|>\n"
70
+ assert _text(spans, 1, "header").startswith("<|im_start|>assistant\n")
71
+ assert "One more." in _text(spans, 3, "content")
72
+ assert "<function=get_time>" in _text(spans, 3, "content")
73
+
74
+
75
+ def test_a_turn_with_no_tool_call_keeps_its_split():
76
+ spans = _spans()
77
+ assert _text(spans, 0, "header") == "<|im_start|>user\n"
78
+ assert _text(spans, 0, "content") == "Find the weather in Paris and in Rome."
79
+ assert _text(spans, 6, "content") == "Paris is sunny; Rome has rain."
80
+ assert _text(spans, 2, "content").strip() == "Sunny"
81
+
82
+
83
+ def test_a_prefill_after_tool_calls_keeps_its_content():
84
+ spans = _spans(prefill=True)
85
+ assert spans[-1].message_index == 6
86
+ assert _text(spans, 6, "content") == "Paris is sunny; Rome has rain."
87
+ assert "<function=get_weather>" in _text(spans, 1, "content")
File without changes
File without changes