coderouter-cli 2.11.2__tar.gz → 2.12.0__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 (300) hide show
  1. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/CHANGELOG.md +204 -0
  2. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/PKG-INFO +1 -1
  3. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/config/schemas.py +50 -0
  4. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/context_budget.py +165 -20
  5. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/launcher_routes.py +11 -1
  6. coderouter_cli-2.12.0/coderouter/token_estimation.py +441 -0
  7. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/context-budget.en.md +39 -2
  8. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/context-budget.md +39 -2
  9. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/subagent-routing.en.md +2 -0
  10. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/subagent-routing.md +2 -0
  11. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers-multiagent.yaml +9 -1
  12. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.raspberrypi.yaml +26 -0
  13. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.yaml +11 -2
  14. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/pyproject.toml +37 -2
  15. coderouter_cli-2.12.0/tests/__init__.py +0 -0
  16. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/conftest.py +23 -0
  17. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_auto_router.py +75 -0
  18. coderouter_cli-2.12.0/tests/test_context_budget.py +877 -0
  19. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_ingress_count_tokens.py +135 -0
  20. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_backend_variants.py +5 -1
  21. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_gui_devices.py +5 -4
  22. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_gui_readiness.py +4 -3
  23. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_variant_gui.py +7 -4
  24. coderouter_cli-2.12.0/tests/test_packaging.py +245 -0
  25. coderouter_cli-2.12.0/tests/test_token_estimation.py +456 -0
  26. coderouter_cli-2.11.2/coderouter/token_estimation.py +0 -208
  27. coderouter_cli-2.11.2/tests/test_context_budget.py +0 -404
  28. coderouter_cli-2.11.2/tests/test_token_estimation.py +0 -135
  29. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/.gitignore +0 -0
  30. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/LICENSE +0 -0
  31. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/README.en.md +0 -0
  32. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/README.md +0 -0
  33. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/__init__.py +0 -0
  34. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/__main__.py +0 -0
  35. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/adapters/__init__.py +0 -0
  36. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/adapters/anthropic_native.py +0 -0
  37. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/adapters/base.py +0 -0
  38. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/adapters/openai_compat.py +0 -0
  39. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/adapters/registry.py +0 -0
  40. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/cli.py +0 -0
  41. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/cli_stats.py +0 -0
  42. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/config/__init__.py +0 -0
  43. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/config/capability_registry.py +0 -0
  44. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/config/env_file.py +0 -0
  45. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/config/loader.py +0 -0
  46. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/cost.py +0 -0
  47. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/data/__init__.py +0 -0
  48. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/data/model-capabilities.yaml +0 -0
  49. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/doctor.py +0 -0
  50. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/doctor_apply.py +0 -0
  51. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/env_security.py +0 -0
  52. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/errors.py +0 -0
  53. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/gguf_introspect.py +0 -0
  54. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/__init__.py +0 -0
  55. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/_fingerprint.py +0 -0
  56. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/backend_health.py +0 -0
  57. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/continuous_probe.py +0 -0
  58. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/drift_actions.py +0 -0
  59. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/drift_detection.py +0 -0
  60. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/memory_budget.py +0 -0
  61. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/memory_pressure.py +0 -0
  62. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/self_healing.py +0 -0
  63. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/tool_loop.py +0 -0
  64. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/hardware.py +0 -0
  65. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/__init__.py +0 -0
  66. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/anthropic_routes.py +0 -0
  67. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/app.py +0 -0
  68. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/dashboard_routes.py +0 -0
  69. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/metrics_routes.py +0 -0
  70. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/openai_routes.py +0 -0
  71. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/language_tax.py +0 -0
  72. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/launcher_devices.py +0 -0
  73. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/launcher_speculative.py +0 -0
  74. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/launcher_swap.py +0 -0
  75. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/logging.py +0 -0
  76. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/metrics/__init__.py +0 -0
  77. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/metrics/collector.py +0 -0
  78. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/metrics/prometheus.py +0 -0
  79. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/output_filters.py +0 -0
  80. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/plugins/__init__.py +0 -0
  81. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/plugins/base.py +0 -0
  82. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/plugins/loader.py +0 -0
  83. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/plugins/registry.py +0 -0
  84. /coderouter_cli-2.11.2/tests/__init__.py → /coderouter_cli-2.12.0/coderouter/py.typed +0 -0
  85. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/routing/__init__.py +0 -0
  86. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/routing/adaptive.py +0 -0
  87. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/routing/auto_router.py +0 -0
  88. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/routing/budget.py +0 -0
  89. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/routing/capability.py +0 -0
  90. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/routing/fallback.py +0 -0
  91. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/state/__init__.py +0 -0
  92. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/state/audit_log.py +0 -0
  93. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/state/replay.py +0 -0
  94. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/state/request_log.py +0 -0
  95. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/state/store.py +0 -0
  96. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/state/suggest_rules.py +0 -0
  97. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/token_estimation_accurate.py +0 -0
  98. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/translation/__init__.py +0 -0
  99. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/translation/anthropic.py +0 -0
  100. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/translation/convert.py +0 -0
  101. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/translation/tool_repair.py +0 -0
  102. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/vscode_init.py +0 -0
  103. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/README.en.md +0 -0
  104. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/README.md +0 -0
  105. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/assets/dashboard-demo.png +0 -0
  106. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
  107. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
  108. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/external-agents.en.md +0 -0
  109. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/external-agents.md +0 -0
  110. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/gguf_dl.en.md +0 -0
  111. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/gguf_dl.md +0 -0
  112. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/hf-ollama-models.en.md +0 -0
  113. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/hf-ollama-models.md +0 -0
  114. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/install-backends.en.md +0 -0
  115. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/install-backends.md +0 -0
  116. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/launcher-quickstart.en.md +0 -0
  117. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/launcher-quickstart.md +0 -0
  118. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/launcher.en.md +0 -0
  119. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/launcher.md +0 -0
  120. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/llamacpp-direct.en.md +0 -0
  121. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/llamacpp-direct.md +0 -0
  122. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/lmstudio-direct.en.md +0 -0
  123. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/lmstudio-direct.md +0 -0
  124. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/verify-ollama-0.23.1.md +0 -0
  125. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/architecture.en.md +0 -0
  126. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/architecture.md +0 -0
  127. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/continuous-probing.en.md +0 -0
  128. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/continuous-probing.md +0 -0
  129. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/drift-detection.en.md +0 -0
  130. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/drift-detection.md +0 -0
  131. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/partial-stitch.en.md +0 -0
  132. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/partial-stitch.md +0 -0
  133. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/agent-cli-plugin-extraction.md +0 -0
  134. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/external-agents-adapter.md +0 -0
  135. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/launcher-model-swap.md +0 -0
  136. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/launcher-multi-build.md +0 -0
  137. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/orchestration-companion.md +0 -0
  138. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  139. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/v1.6-auto-router-verification.md +0 -0
  140. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/v1.6-auto-router.md +0 -0
  141. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/free-tier-guide.en.md +0 -0
  142. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/free-tier-guide.md +0 -0
  143. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/language-tax.en.md +0 -0
  144. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/language-tax.md +0 -0
  145. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/remote-access.en.md +0 -0
  146. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/remote-access.md +0 -0
  147. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/security.en.md +0 -0
  148. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/security.md +0 -0
  149. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/troubleshooting.en.md +0 -0
  150. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/troubleshooting.md +0 -0
  151. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/usage-guide.en.md +0 -0
  152. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/usage-guide.md +0 -0
  153. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/vscode.md +0 -0
  154. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/low-memory-integration.en.md +0 -0
  155. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/low-memory-integration.md +0 -0
  156. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/openrouter-roster/CHANGES.md +0 -0
  157. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/openrouter-roster/README.md +0 -0
  158. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/openrouter-roster/latest.json +0 -0
  159. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v0.4.md +0 -0
  160. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v0.5-verify.md +0 -0
  161. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v0.5.md +0 -0
  162. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v0.6.md +0 -0
  163. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v0.7.md +0 -0
  164. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v1.0-verify.md +0 -0
  165. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v1.0.md +0 -0
  166. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/start/quickstart.en.md +0 -0
  167. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/start/quickstart.md +0 -0
  168. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/start/when-do-i-need-coderouter.en.md +0 -0
  169. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/start/when-do-i-need-coderouter.md +0 -0
  170. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/.env.example +0 -0
  171. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/README.md +0 -0
  172. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers-agent-cli.yaml +0 -0
  173. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.context-budget-test.yaml +0 -0
  174. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.llamacpp-vllm.yaml +0 -0
  175. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.nvidia-nim.yaml +0 -0
  176. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.ollama-auto-custom.yaml +0 -0
  177. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.ollama-auto.yaml +0 -0
  178. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.ollama-free-chain.yaml +0 -0
  179. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.swap.yaml +0 -0
  180. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/demo_traffic.sh +0 -0
  181. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/openrouter_roster_diff.py +0 -0
  182. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/smoke_v2_2.sh +0 -0
  183. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/verify-providers.yaml +0 -0
  184. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/verify_ollama_0_23.py +0 -0
  185. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/verify_v0_5.sh +0 -0
  186. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/verify_v1_0.sh +0 -0
  187. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_adapter_anthropic.py +0 -0
  188. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_agent_cli.py +0 -0
  189. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_audit_log.py +0 -0
  190. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_auto_router_cjk.py +0 -0
  191. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_backend_health.py +0 -0
  192. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_budget.py +0 -0
  193. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_capability.py +0 -0
  194. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_capability_degraded_payload.py +0 -0
  195. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_capability_registry.py +0 -0
  196. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_capability_registry_cache_control.py +0 -0
  197. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_capability_tool_choice.py +0 -0
  198. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_claude_code_suitability.py +0 -0
  199. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_cli.py +0 -0
  200. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_cli_bind_warning.py +0 -0
  201. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_cli_stats.py +0 -0
  202. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_config.py +0 -0
  203. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_config_cwd_warning.py +0 -0
  204. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_config_shim_actions.py +0 -0
  205. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_continuous_probe.py +0 -0
  206. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_dashboard_endpoint.py +0 -0
  207. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_doctor.py +0 -0
  208. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_doctor_apply.py +0 -0
  209. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_doctor_cache_probe.py +0 -0
  210. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_drift_actions.py +0 -0
  211. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_drift_detection.py +0 -0
  212. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_drift_detection_integration.py +0 -0
  213. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_env_file.py +0 -0
  214. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_env_security.py +0 -0
  215. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_errors.py +0 -0
  216. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_examples_yaml.py +0 -0
  217. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_examples_yaml_roundtrip.py +0 -0
  218. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback.py +0 -0
  219. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_anthropic.py +0 -0
  220. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_cache_control.py +0 -0
  221. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_cache_observed.py +0 -0
  222. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_cache_strip.py +0 -0
  223. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_empty_response.py +0 -0
  224. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_misconfig_warn.py +0 -0
  225. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_paid_gate.py +0 -0
  226. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_thinking.py +0 -0
  227. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_tool_choice_emulate.py +0 -0
  228. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_h1_prometheus_drift.py +0 -0
  229. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_h2_h6_translation.py +0 -0
  230. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_h3_client_reuse.py +0 -0
  231. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_h4_h5_guards.py +0 -0
  232. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_h7_probe_restore.py +0 -0
  233. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_h8_launcher_auth.py +0 -0
  234. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_mA_routing.py +0 -0
  235. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_mB_adapters.py +0 -0
  236. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_mC_convert.py +0 -0
  237. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_mD_logging_metrics.py +0 -0
  238. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_mE_config_validation.py +0 -0
  239. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_mF_ingress.py +0 -0
  240. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_gguf_introspect.py +0 -0
  241. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_guards_tool_loop.py +0 -0
  242. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_hardware.py +0 -0
  243. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_import_hygiene.py +0 -0
  244. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_ingress_anthropic.py +0 -0
  245. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_ingress_profile.py +0 -0
  246. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_language_tax.py +0 -0
  247. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_language_tax_integration.py +0 -0
  248. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_config_bench.py +0 -0
  249. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_devices.py +0 -0
  250. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_devices_detect.py +0 -0
  251. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_devices_routes.py +0 -0
  252. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_gui_events.py +0 -0
  253. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_mtp.py +0 -0
  254. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_mtp_fallback.py +0 -0
  255. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_provider_sync.py +0 -0
  256. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_readiness_restart.py +0 -0
  257. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_swap.py +0 -0
  258. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_swap_review.py +0 -0
  259. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_variant_config.py +0 -0
  260. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_variant_routes.py +0 -0
  261. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_variant_sweep.py +0 -0
  262. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_memory_budget.py +0 -0
  263. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_memory_pressure.py +0 -0
  264. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_cache.py +0 -0
  265. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_collector.py +0 -0
  266. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_cost.py +0 -0
  267. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_endpoint.py +0 -0
  268. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_jsonl.py +0 -0
  269. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_prometheus.py +0 -0
  270. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_prometheus_cache.py +0 -0
  271. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_models_passthrough.py +0 -0
  272. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_openai_compat.py +0 -0
  273. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_openrouter_roster_diff.py +0 -0
  274. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_output_filters.py +0 -0
  275. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_output_filters_adapters.py +0 -0
  276. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_partial_stitch.py +0 -0
  277. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_plugin_adapter.py +0 -0
  278. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_plugins_integration.py +0 -0
  279. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_plugins_loader.py +0 -0
  280. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_plugins_registry.py +0 -0
  281. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_reasoning_strip.py +0 -0
  282. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_repair_byte_fallback.py +0 -0
  283. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_request_log.py +0 -0
  284. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_role_normalization.py +0 -0
  285. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_routing_adaptive.py +0 -0
  286. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_self_healing.py +0 -0
  287. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_setup_sh.py +0 -0
  288. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_state_store.py +0 -0
  289. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_stop_reason_forward_compat.py +0 -0
  290. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_token_estimation_accurate.py +0 -0
  291. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_tokens_saved_metric.py +0 -0
  292. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_tool_repair.py +0 -0
  293. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_tool_repair_differential.py +0 -0
  294. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_tool_repair_lenient.py +0 -0
  295. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_tool_repair_r4.py +0 -0
  296. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_tool_repair_scanners.py +0 -0
  297. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_toolrepair_bench.py +0 -0
  298. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_translation_anthropic.py +0 -0
  299. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_translation_reverse.py +0 -0
  300. {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_vscode_init.py +0 -0
@@ -9,6 +9,210 @@ are kept verbatim where the Japanese text itself is the subject).
9
9
 
10
10
  ---
11
11
 
12
+ ## [v2.12.0] — 2026-08-04 (the context-budget guard actually fires now)
13
+
14
+ **This release changes behaviour you can observe.** The token estimator
15
+ counted `tool_result`, `tool_use` and `thinking` blocks as zero
16
+ characters, which meant the L1 context-budget guard stayed silent on
17
+ exactly the sessions it exists to protect — agent conversations, where
18
+ tool output is most of the context. Fixing the estimator makes the guard
19
+ fire, the auto-router's `content_token_count_min` rules match earlier,
20
+ and `POST /v1/messages/count_tokens` return larger numbers.
21
+
22
+ Nothing was removed and no field changed meaning, so configs load
23
+ unchanged. But if you set `context_budget_action: trim`, or wrote a
24
+ `content_token_count_min` threshold, **read the migration note below
25
+ before upgrading.** `token_estimation_include_tool_content: false`
26
+ restores the v2.11.x estimate exactly.
27
+
28
+ ### Fixed
29
+
30
+ - **The token estimator ignored tool blocks, so the context-budget guard
31
+ never fired for agent sessions.** `_extract_text_from_content` only
32
+ summed `type: "text"` blocks. A `tool_result` carrying 5,000 characters
33
+ of file content counted as 0, and so did a `tool_use` input. In a
34
+ Claude Code shaped session — short assistant text, large tool results —
35
+ that under-counted by 8x at 20 turns, 14x at 50 and **24x at 200**, so
36
+ a conversation genuinely at 96,311 tokens was reported as 4,011 and the
37
+ 0.80 / 0.90 thresholds were never approached. `context_budget_action:
38
+ trim` was, in practice, inert for the workload it was written for. The
39
+ estimator now walks blocks by type: `text`, `tool_result` (recursing
40
+ into a list-shaped `content`), `tool_use` (name plus its serialised
41
+ input) and `thinking` are counted. **`image` blocks and
42
+ `redacted_thinking.data` stay at zero** and this is load-bearing — a
43
+ naive `json.dumps` over the whole block would have inflated a session
44
+ holding one 400 KB base64 PNG by 7.1x. Unknown block types are skipped
45
+ rather than raising, and a self-referential structure terminates
46
+ (`token_estimation.py`).
47
+ - **Trimming could empty the conversation entirely.** `_normalize_head`
48
+ drops leading `assistant` messages and `user` messages that carry a
49
+ `tool_result`, because neither is a valid opening turn. When the
50
+ preserved tail was `[assistant(tool_use), user(tool_result), …]` — the
51
+ normal shape of an agent conversation — every message matched and the
52
+ trimmer returned an empty list. `AnthropicRequest.messages` has no
53
+ `min_length`, so pydantic accepted it and the upstream API answered
54
+ 400. Measured on the shipped code: 81 messages in, 0 out. This was
55
+ reachable before this release too, but only for text-heavy sessions,
56
+ because tool blocks estimated as zero meant trimming rarely engaged.
57
+ Making the estimator honest would have made it routine. There is now
58
+ no path through the trimmer that returns an empty list for a non-empty
59
+ input (`guards/context_budget.py`).
60
+ - **Restoring a head message could undo the entire trim.** When
61
+ normalisation left no valid opening turn, the trimmer restored the most
62
+ recent clean `user` message it had just dropped. If that message was
63
+ the reason the request was over budget — a pasted file, a log dump —
64
+ restoring it put the request straight back over the window while
65
+ reporting `status: "trimmed"`. On a 200 KB paste followed by 25 tool
66
+ turns: 67,066 tokens in, 67,060 out, against a 32,768 window. Across a
67
+ 300-session sweep, **89% of the results that still exceeded the window
68
+ were caused by this restore**, none by the preserve floor. Restoration
69
+ now checks the budget: the most recent *affordable* dropped clean user
70
+ wins, and if none fits, a synthetic
71
+ `[earlier conversation trimmed to fit the context budget]` head is
72
+ used instead. Same fixture: 67,066 → 17,062, inside both the window and
73
+ the trim target. The sweep goes to 0% (`guards/context_budget.py`).
74
+ - **The trim loop was quadratic.** It rebuilt the surviving message list
75
+ and re-scanned every character for each unit it considered dropping.
76
+ With the corrected estimator that is 217 ms of blocked event loop for
77
+ an 801-message conversation, and it is called synchronously from the
78
+ request path. Per-message character counts are now computed once and
79
+ decremented as units are dropped: 217 ms → 4 ms. Units that would free
80
+ nothing are skipped rather than dropped, so the trimmer no longer
81
+ destroys history without reducing the estimate. The set of dropped
82
+ messages is unchanged — verified against the old algorithm across 200
83
+ random conversations (`guards/context_budget.py`).
84
+ - **The readiness probe now targets the `127.0.0.1` literal rather than
85
+ `localhost`.** Both copies of `_backend_ready` fetched
86
+ `http://localhost:<port>/health` while the backends they probe listen
87
+ on IPv4 only (`llama-server --host 127.0.0.1` by default), and the bare
88
+ TCP fallback in the same function already used the literal — so the
89
+ HTTP branch was the odd one out. On a host with no IPv6 stack at all,
90
+ where `localhost` resolves to `::1` and nothing else, this fails every
91
+ attempt with `Address family not supported` and a backend that is up
92
+ and serving never registers. Hardening rather than a reported bug: on a
93
+ host that has `::1`, the connect is refused immediately and the
94
+ resolver falls through to IPv4, so the old form worked. Applied to both
95
+ `ingress/launcher_routes.py` and `launcher_gui.py`, which carry
96
+ independent copies of this function.
97
+
98
+ ### Added
99
+
100
+ - **`token_estimation_include_tool_content`** (top level, default
101
+ `true`). Set it to `false` to get the v2.11.x estimate back, byte for
102
+ byte — verified identical across 300 fuzzed request bodies on all three
103
+ estimator entry points. It is an escape hatch for operators who tuned
104
+ thresholds against the old numbers and need time to retune; expect it
105
+ to be removed in a future major. It is deliberately not per-profile:
106
+ the auto-router runs *before* a profile is chosen, and `count_tokens`
107
+ answers a client question that must not depend on which chain would
108
+ have served the request (`config/schemas.py`).
109
+ - **`coderouter/py.typed`.** The package has advertised
110
+ `Typing :: Typed` in its classifiers without shipping a PEP 561
111
+ marker, so type checkers treated it as untyped. Verified present in the
112
+ built wheel (`pyproject.toml`).
113
+ - **Release-time version and changelog checks.** `release.yml` now fails
114
+ the build if the pushed tag does not match `pyproject.toml`'s version,
115
+ or if `CHANGELOG.md` has no section for it — the awk that extracts
116
+ release notes silently falls back to a placeholder otherwise. Skipped
117
+ on `workflow_dispatch`, where `github.ref_name` is a branch.
118
+
119
+ ### Changed
120
+
121
+ - **`examples/providers.yaml` and `examples/providers-multiagent.yaml`
122
+ now ship `context_budget_action: warn` instead of `trim`.** README
123
+ tells new users to `curl` the first of those, so shipping `trim`
124
+ together with a newly-working guard would have started deleting
125
+ history in conversations for people who never opted into it. Warn
126
+ first, read the logs, then decide. `docs/concepts/context-budget.md`
127
+ was updated to match.
128
+ - **`thinking` blocks are now counted.** Extended-thinking content is
129
+ replayed to the model on the following turn — it has to be, or tool-use
130
+ signatures break — so it occupies the window exactly like text.
131
+ `redacted_thinking.data` is still not counted: it is opaque ciphertext,
132
+ the same category as base64 image data.
133
+ - **Third-party GitHub Actions are pinned to commit SHAs**, with the
134
+ version in a trailing comment so dependabot still tracks them. The
135
+ publish job holds `id-token: write` for Trusted Publishing and the
136
+ release job holds `contents: write`; a moved tag on any action they
137
+ use is a direct path to the PyPI project.
138
+ - **A macOS CI runner was added and then withdrawn again before this
139
+ release shipped.** It failed six launcher tests immediately, and the
140
+ failures turned out to belong to the test harness rather than the
141
+ product: every test that spawns a real stub process and requires its
142
+ `/health` to answer 200 within the harness's 5.0s
143
+ `readiness_timeout_s` failed, and no others. Spawning, argv
144
+ construction, crash detection, SIGTERM handling, TTL and registry
145
+ behaviour all passed on macOS. That 5.0s budget is 1/60th of the
146
+ shipped default of 300.0s, so it does not represent any real
147
+ deployment. Rather than ship six `xfail`s or a red CI, the runner is
148
+ deferred to a follow-up that raises the harness budget first. The
149
+ reasoning and the re-enable steps are recorded in `ci.yml` next to the
150
+ matrix.
151
+ - **Corrected the note about tkinter on CI**, which v2.11.2 got wrong in
152
+ both directions. The GUI test modules originally said the uv-managed
153
+ Python on CI has no tkinter; v2.11.2 "fixed" that to say it does,
154
+ based on measuring `uv python install 3.12.11` in a Linux container.
155
+ The actual CI runs settle it: the ubuntu runner skips those four files
156
+ and the macOS runner ran them. Tk availability is a property of the
157
+ runner image, so the docstrings no longer assert it in either
158
+ direction — they just say to keep the `importorskip`.
159
+ - **The CVE audit covers every extra.** `uv export` passed only
160
+ `--extra dev`, leaving `accuracy` (tokenizers) and `repair`
161
+ (json-repair) — both installable by users — outside pip-audit's view.
162
+ Now `--all-extras`.
163
+ - **The sdist no longer allow-lists `docs` wholesale.** `only-include`
164
+ named the whole directory, so a local `uv build` swept in
165
+ `docs/inside/` and `docs/articles/` — private notes and article drafts
166
+ that `.gitignore` exists to keep out. A CI build from a clean checkout
167
+ was never affected, since those files are untracked, but a maintainer
168
+ building from a working tree would have shipped them. The list now
169
+ names the public subdirectories, and `release.yml` fails the build if
170
+ either directory reappears in the tarball. Every entry has to be
171
+ git-tracked: `docs/evidence` exists only in a maintainer's working tree
172
+ and made the allowlist claim to ship something a fresh checkout does
173
+ not have. `tests/test_packaging.py` now asserts each listed path
174
+ exists, which is how that was caught.
175
+
176
+ ### Migration
177
+
178
+ Only two settings change meaning. If you set neither, upgrading is a
179
+ no-op beyond larger `count_tokens` responses.
180
+
181
+ **`context_budget_action: trim` or `warn`** — the guard was effectively
182
+ inert for tool-heavy sessions and now works. Expect
183
+ `context-budget-warning` in the logs, the
184
+ `X-CodeRouter-Context-Budget` response header, and — under `trim` —
185
+ conversation history actually being dropped, with a
186
+ `[earlier conversation trimmed to fit the context budget]` message
187
+ appearing at the head when nothing else fits. Measured against a 32 K
188
+ window, a tool-driven session now warns around turn 32 and trims around
189
+ turn 36; before, it never did either. Start with `warn` and read the
190
+ logs before enabling `trim`.
191
+
192
+ **`content_token_count_min`** — auto-router rules using it will match far
193
+ earlier. A tool-driven session that reported 1,083 tokens after 200 turns
194
+ under v2.11.x reports 93,183 now, so a `32000` threshold moves from never
195
+ firing to firing around turn 69. Check where such a rule routes:
196
+ `examples/providers.raspberrypi.yaml` sends matching traffic to a cloud
197
+ free tier rather than the local model. Retune the threshold, or set
198
+ `token_estimation_include_tool_content: false` while you do.
199
+
200
+ ### Tests
201
+
202
+ - 3 new files, 48 new cases. Suite: 2393 passed, 1 skipped, ruff clean.
203
+ - Each fix has a regression test confirmed to fail against the pre-fix
204
+ tree, including `test_trim_never_returns_empty_messages` (81 in, 0 out
205
+ on the old code) and `test_trim_lands_inside_the_window_across_budgets`,
206
+ which asserts that if a trimmed request is still over the window, the
207
+ preserve floor alone must account for it — structurally forbidding the
208
+ restore branch from being the cause.
209
+ - The incremental trim loop is pinned against the old full-recompute
210
+ implementation across 200 random conversations (identical dropped
211
+ sets), and the opt-out is pinned against v2.11.2 across 300 fuzzed
212
+ bodies.
213
+
214
+ ---
215
+
12
216
  ## [v2.11.2] — 2026-08-04 (file-write safety and launcher event hygiene)
13
217
 
14
218
  Two fixes from the same review round as v2.11.1, split out because they
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.11.2
3
+ Version: 2.12.0
4
4
  Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
5
5
  Project-URL: Homepage, https://github.com/zephel01/CodeRouter
6
6
  Project-URL: Repository, https://github.com/zephel01/CodeRouter
@@ -27,6 +27,7 @@ from pydantic import (
27
27
  )
28
28
 
29
29
  from coderouter.logging import get_logger
30
+ from coderouter.token_estimation import set_include_tool_content
30
31
 
31
32
  logger = get_logger(__name__)
32
33
 
@@ -2306,6 +2307,36 @@ class CodeRouterConfig(BaseModel):
2306
2307
  ),
2307
2308
  )
2308
2309
 
2310
+ # H-5 escape hatch: token-estimation scope.
2311
+ #
2312
+ # Deliberately top-level rather than per-``FallbackChain``, even
2313
+ # though ``context_budget_action`` lives on the chain. The shared
2314
+ # char/4 estimator has four consumers and three of them run with no
2315
+ # profile in hand: the auto-router runs *before* a profile exists
2316
+ # (it is what picks one), ``POST /v1/messages/count_tokens`` answers
2317
+ # a client question that must not depend on which chain would have
2318
+ # served it, and language-tax measurement is profile-agnostic. A
2319
+ # per-profile switch would let the router and the guard disagree
2320
+ # about the size of the very same request — exactly the class of bug
2321
+ # this key exists to let operators back out of.
2322
+ token_estimation_include_tool_content: bool = Field(
2323
+ default=True,
2324
+ description=(
2325
+ "v2.12 (H-5): count ``tool_result`` / ``tool_use`` / "
2326
+ "``thinking`` content blocks in the shared char/4 token "
2327
+ "estimator. True (default) is the correct behavior — up to "
2328
+ "v2.11.x these blocks estimated to 0 chars, under-counting a "
2329
+ "Claude Code style session by 5x at 20 turns and ~29x at 200. "
2330
+ "Image blocks stay at 0 either way. Set to false as an escape "
2331
+ "hatch to restore v2.11.x-identical estimates (and therefore "
2332
+ "v2.11.x context-budget firing, auto-router "
2333
+ "``content_token_count_min`` matching and "
2334
+ "``/v1/messages/count_tokens`` numbers) if the corrected "
2335
+ "estimate disrupts a tuned deployment. Compatibility shim "
2336
+ "only — scheduled for removal in a future release."
2337
+ ),
2338
+ )
2339
+
2309
2340
  # v2.0-I: Continuous probing — background health checks for idle periods.
2310
2341
  continuous_probe: Literal["off", "active"] = Field(
2311
2342
  default="off",
@@ -2816,6 +2847,25 @@ class CodeRouterConfig(BaseModel):
2816
2847
  )
2817
2848
  return self
2818
2849
 
2850
+ @model_validator(mode="after")
2851
+ def _apply_token_estimation_scope(self) -> CodeRouterConfig:
2852
+ """Publish ``token_estimation_include_tool_content`` process-wide.
2853
+
2854
+ The char/4 estimator is a dependency-free leaf module that every
2855
+ consumer imports directly as a plain function; there is no
2856
+ object graph to thread a flag through, and three of the four
2857
+ consumers have no profile/config handle at their call site. So
2858
+ the loaded config pushes the setting into the module once, here.
2859
+
2860
+ Idempotent and deterministic: the value always reflects the most
2861
+ recently constructed config, which in a server process is the
2862
+ single config that was loaded at startup. Declared last so a
2863
+ config that fails any other validator never takes effect. Tests
2864
+ that flip it should restore the default (True) afterwards.
2865
+ """
2866
+ set_include_tool_content(self.token_estimation_include_tool_content)
2867
+ return self
2868
+
2819
2869
  def provider_by_name(self, name: str) -> ProviderConfig:
2820
2870
  """Look up a provider config by name. Raises KeyError if not found."""
2821
2871
  for p in self.providers:
@@ -38,6 +38,11 @@ Uses the shared :func:`~coderouter.token_estimation.estimate_tokens_from_anthrop
38
38
  (char/4 heuristic, 5-deps invariant). See that module's docstring
39
39
  for the CJK caveat and recommended threshold compensation.
40
40
 
41
+ Since H-5 the estimator also counts ``tool_result`` / ``tool_use`` /
42
+ ``thinking`` blocks, so a Claude-Code-shaped session now reports a
43
+ number 5-29x larger than it did in v2.11.x — i.e. the guard finally
44
+ fires on the sessions it was written for.
45
+
41
46
  Trim algorithm
42
47
  ==============
43
48
 
@@ -49,6 +54,16 @@ Trim algorithm
49
54
  ``tool_use`` assistant message (and vice versa).
50
55
  5. After removal, re-estimate; if still over ``trim_target``,
51
56
  reduce ``preserve_last_n`` by 1 and retry (minimum floor: 2).
57
+ 6. The result is **never** an empty message list — see
58
+ :func:`_normalize_head`.
59
+
60
+ Cost
61
+ ====
62
+
63
+ The incremental loop pre-computes each message's character count once
64
+ and subtracts as units are dropped, so a trim is O(total chars) rather
65
+ than O(messages x total chars). Before that change an 800-message trim
66
+ blocked the event loop for ~0.2s.
52
67
  """
53
68
 
54
69
  from __future__ import annotations
@@ -57,6 +72,9 @@ from dataclasses import dataclass
57
72
  from typing import TYPE_CHECKING, Any
58
73
 
59
74
  from coderouter.token_estimation import (
75
+ chars_to_tokens,
76
+ count_message_chars,
77
+ count_system_chars,
60
78
  estimate_tokens_from_anthropic_request,
61
79
  )
62
80
 
@@ -398,7 +416,37 @@ def _group_removable_units(
398
416
  return units
399
417
 
400
418
 
401
- def _normalize_head(messages: list[Any]) -> list[Any]:
419
+ def _role_of(msg: Any) -> Any:
420
+ """Role of a message (Pydantic model or dict)."""
421
+ if hasattr(msg, "role"):
422
+ return msg.role
423
+ if isinstance(msg, dict):
424
+ return msg.get("role")
425
+ return None
426
+
427
+
428
+ def _is_clean_user(msg: Any) -> bool:
429
+ """True for a ``user`` message that does not open with a tool_result."""
430
+ return _role_of(msg) == "user" and not _has_tool_result(msg)
431
+
432
+
433
+ #: Content of the synthetic head inserted when no real clean ``user``
434
+ #: message can be re-attached within budget. Short on purpose (~17
435
+ #: tokens): its only job is to give Anthropic the valid opening turn the
436
+ #: wire format demands while telling the model why history is missing.
437
+ TRIM_PLACEHOLDER_HEAD_TEXT = "[earlier conversation trimmed to fit the context budget]"
438
+
439
+
440
+ def _synthetic_head() -> dict[str, Any]:
441
+ """A minimal, always-affordable clean ``user`` message."""
442
+ return {"role": "user", "content": TRIM_PLACEHOLDER_HEAD_TEXT}
443
+
444
+
445
+ def _normalize_head(
446
+ messages: list[Any],
447
+ *,
448
+ fallback_head: Any | None = None,
449
+ ) -> list[Any]:
402
450
  """Drop leading messages until the first is a clean ``user`` message.
403
451
 
404
452
  Anthropic requires the first message to be a ``user`` message, and a
@@ -407,18 +455,77 @@ def _normalize_head(messages: list[Any]) -> list[Any]:
407
455
  any assistant message and any user message that contains a
408
456
  tool_result, stopping at the first ``user`` message with no
409
457
  tool_result block.
458
+
459
+ **Never returns an empty list for a non-empty input.** In a
460
+ tool-heavy agent session the preserved tail can legitimately look
461
+ like ``[assistant(tool_use), user(tool_result), assistant(tool_use),
462
+ user(tool_result)]`` — every single message matches the drop
463
+ condition, and the naive loop returned ``[]``. ``AnthropicRequest``
464
+ declares no ``min_length`` on ``messages``, so pydantic happily
465
+ forwarded that and the upstream API answered 400, destroying the
466
+ session the guard exists to protect. (Latent before H-5 only
467
+ because ``tool_result`` blocks estimated to 0 chars, so trim never
468
+ fired on exactly those conversations.)
469
+
470
+ When no surviving message can serve as the head, ``fallback_head``
471
+ is prepended. The caller picks it under the token budget — see
472
+ :func:`_pick_fallback_head`; this function deliberately does no
473
+ budget arithmetic of its own.
410
474
  """
411
- start = 0
412
- n = len(messages)
413
- while start < n:
414
- msg = messages[start]
415
- role = msg.role if hasattr(msg, "role") else (
416
- msg.get("role") if isinstance(msg, dict) else None
417
- )
418
- if role == "user" and not _has_tool_result(msg):
419
- break
420
- start += 1
421
- return messages[start:]
475
+ if not messages:
476
+ return list(messages)
477
+
478
+ for start, msg in enumerate(messages):
479
+ if _is_clean_user(msg):
480
+ return messages[start:]
481
+
482
+ # Every surviving message is an assistant turn or a tool_result-
483
+ # carrying user turn: normalizing by dropping would empty the list.
484
+ if fallback_head is not None:
485
+ return [fallback_head, *messages]
486
+ return list(messages)
487
+
488
+
489
+ def _pick_fallback_head(
490
+ messages: list[Any],
491
+ msg_chars: list[int],
492
+ dropped: set[int],
493
+ *,
494
+ before: int,
495
+ kept_chars: int,
496
+ target_tokens: int,
497
+ ) -> Any:
498
+ """Choose a head message that keeps the result inside the budget.
499
+
500
+ Called only when nothing in the surviving window is a clean ``user``
501
+ message, so *something* has to be put in front or the request is a
502
+ guaranteed 400.
503
+
504
+ Selection order:
505
+
506
+ 1. The **most recent** dropped clean ``user`` message that still
507
+ fits under ``target_tokens`` — recency matters because it is
508
+ usually the operator's latest standing instruction.
509
+ 2. Failing that, a synthetic placeholder (:func:`_synthetic_head`).
510
+
511
+ The budget check is the whole point. Re-attaching the *newest*
512
+ candidate unconditionally is a trap: the most recent clean user
513
+ message in an agent session is very often a huge pasted file, and
514
+ restoring it undoes the entire trim. Measured on the canonical
515
+ "200 KB paste then 25 tool calls" shape, that path turned a
516
+ 67,066-token request into a 67,061-token request against a 24,576
517
+ target — reported as ``status="trimmed"`` and then rejected
518
+ upstream. A budget-blind restore does not avoid the 400, it
519
+ guarantees it.
520
+ """
521
+ for idx in sorted(dropped, reverse=True):
522
+ if idx >= before:
523
+ continue
524
+ if not _is_clean_user(messages[idx]):
525
+ continue
526
+ if chars_to_tokens(kept_chars + msg_chars[idx]) <= target_tokens:
527
+ return messages[idx]
528
+ return _synthetic_head()
422
529
 
423
530
 
424
531
  def _do_trim(
@@ -437,12 +544,23 @@ def _do_trim(
437
544
  hard floor that is never removed. After trimming, the head is
438
545
  normalized so the first surviving message is a ``user`` message
439
546
  without a leading ``tool_result`` (avoids upstream 400s). Bug H5.
547
+
548
+ The loop is incremental in *characters*: every message's character
549
+ contribution is computed exactly once up front and subtracted as
550
+ units are dropped, instead of rebuilding the kept list and
551
+ re-walking the whole conversation on every iteration (the old
552
+ O(messages x chars) shape cost ~0.2s of event-loop time for an
553
+ 800-message session). Because the char/4 heuristic floor-divides
554
+ the grand total exactly once, the running sum stays bit-identical
555
+ to a full recompute.
440
556
  """
441
557
  if not messages:
442
558
  return messages
443
559
 
444
- already = estimate_tokens_from_anthropic_request(system=system, messages=messages)
445
- if already <= target_tokens:
560
+ system_chars = count_system_chars(system)
561
+ msg_chars = [count_message_chars(m) for m in messages]
562
+ total_chars = system_chars + sum(msg_chars)
563
+ if chars_to_tokens(total_chars) <= target_tokens:
446
564
  # Nothing to do — return the input unchanged.
447
565
  return messages
448
566
 
@@ -453,14 +571,41 @@ def _do_trim(
453
571
  # Indices we have decided to drop; peel atomic units from the front.
454
572
  dropped: set[int] = set()
455
573
  for unit in units:
456
- kept = [messages[i] for i in range(len(messages)) if i not in dropped]
457
- estimated = estimate_tokens_from_anthropic_request(system=system, messages=kept)
458
- if estimated <= target_tokens:
574
+ if chars_to_tokens(total_chars) <= target_tokens:
459
575
  break
576
+ unit_chars = sum(msg_chars[i] for i in unit)
577
+ if unit_chars <= 0:
578
+ # Dropping this unit would not move the estimate at all
579
+ # (e.g. an image-only turn, which counts as 0 chars by
580
+ # design). Destroying history for no budget gain is pure
581
+ # loss — skip it and try the next unit.
582
+ continue
460
583
  dropped.update(unit)
461
-
462
- trimmed = [messages[i] for i in range(len(messages)) if i not in dropped]
463
- return _normalize_head(trimmed)
584
+ total_chars -= unit_chars
585
+
586
+ kept_indices = [i for i in range(len(messages)) if i not in dropped]
587
+ trimmed = [messages[i] for i in kept_indices]
588
+ if not trimmed:
589
+ # Structural fail-safe: never hand back an empty history.
590
+ return list(messages[-max(preserve_last_n, 1) :])
591
+
592
+ if any(_is_clean_user(m) for m in trimmed):
593
+ # A real head survives — no fallback needed, and computing one
594
+ # would only cost time.
595
+ return _normalize_head(trimmed)
596
+
597
+ # Nothing in the surviving window can open the request. Pick a head
598
+ # that fits the remaining budget (``total_chars`` already reflects
599
+ # every drop above), falling back to a synthetic marker.
600
+ fallback_head = _pick_fallback_head(
601
+ messages,
602
+ msg_chars,
603
+ dropped,
604
+ before=kept_indices[0],
605
+ kept_chars=total_chars,
606
+ target_tokens=target_tokens,
607
+ )
608
+ return _normalize_head(trimmed, fallback_head=fallback_head)
464
609
 
465
610
 
466
611
  __all__ = [
@@ -714,11 +714,21 @@ async def _backend_ready(backend: str, port: int, *, probe_timeout_s: float) ->
714
714
  Falling through to the bare TCP connect here would silently re-introduce
715
715
  the bug readiness gating was added to fix (provider registered before the
716
716
  model finished loading).
717
+
718
+ The probe targets ``127.0.0.1`` literally, never ``localhost``. On a host
719
+ where ``localhost`` resolves to ``::1`` first — the default on GitHub's
720
+ macOS runners, and common on Macs generally — an httpx request to
721
+ ``http://localhost:<port>`` fails with ``Address family not supported``
722
+ against a backend listening on IPv4 only, which is what ``llama-server``
723
+ does by default (``--host 127.0.0.1``). Readiness then never succeeds and
724
+ the spawn times out with ``status='loading'`` even though the backend is
725
+ up and serving. The bare TCP fallback below already used ``127.0.0.1``;
726
+ this branch was the odd one out.
717
727
  """
718
728
  if base_backend(backend) in ("llama.cpp", "vllm"):
719
729
  try:
720
730
  async with httpx.AsyncClient(timeout=probe_timeout_s) as client:
721
- resp = await client.get(f"http://localhost:{port}/health")
731
+ resp = await client.get(f"http://127.0.0.1:{port}/health")
722
732
  return resp.status_code == 200
723
733
  except Exception:
724
734
  return False