coderouter-cli 2.7.0__tar.gz → 2.7.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 (236) hide show
  1. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/.gitignore +4 -0
  2. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/CHANGELOG.md +105 -0
  3. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/PKG-INFO +28 -9
  4. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/README.md +27 -8
  5. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/config/capability_registry.py +22 -0
  6. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/config/schemas.py +78 -0
  7. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/ingress/anthropic_routes.py +108 -0
  8. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/logging.py +5 -0
  9. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/routing/capability.py +217 -0
  10. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/routing/fallback.py +152 -0
  11. coderouter_cli-2.7.2/coderouter/translation/tool_repair.py +1522 -0
  12. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/pyproject.toml +1 -1
  13. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_capability_degraded_payload.py +6 -4
  14. coderouter_cli-2.7.2/tests/test_capability_tool_choice.py +251 -0
  15. coderouter_cli-2.7.2/tests/test_config_shim_actions.py +49 -0
  16. coderouter_cli-2.7.2/tests/test_fallback_cache_strip.py +254 -0
  17. coderouter_cli-2.7.2/tests/test_fallback_tool_choice_emulate.py +267 -0
  18. coderouter_cli-2.7.2/tests/test_ingress_count_tokens.py +150 -0
  19. coderouter_cli-2.7.2/tests/test_tool_repair_lenient.py +388 -0
  20. coderouter_cli-2.7.2/tests/test_tool_repair_r4.py +511 -0
  21. coderouter_cli-2.7.2/tests/test_toolrepair_bench.py +86 -0
  22. coderouter_cli-2.7.0/coderouter/translation/tool_repair.py +0 -293
  23. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/LICENSE +0 -0
  24. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/README.en.md +0 -0
  25. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/__init__.py +0 -0
  26. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/__main__.py +0 -0
  27. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/adapters/__init__.py +0 -0
  28. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/adapters/anthropic_native.py +0 -0
  29. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/adapters/base.py +0 -0
  30. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/adapters/openai_compat.py +0 -0
  31. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/adapters/registry.py +0 -0
  32. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/cli.py +0 -0
  33. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/cli_stats.py +0 -0
  34. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/config/__init__.py +0 -0
  35. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/config/env_file.py +0 -0
  36. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/config/loader.py +0 -0
  37. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/cost.py +0 -0
  38. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/data/__init__.py +0 -0
  39. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/data/model-capabilities.yaml +0 -0
  40. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/doctor.py +0 -0
  41. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/doctor_apply.py +0 -0
  42. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/env_security.py +0 -0
  43. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/errors.py +0 -0
  44. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/gguf_introspect.py +0 -0
  45. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/guards/__init__.py +0 -0
  46. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/guards/_fingerprint.py +0 -0
  47. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/guards/backend_health.py +0 -0
  48. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/guards/context_budget.py +0 -0
  49. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/guards/continuous_probe.py +0 -0
  50. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/guards/drift_actions.py +0 -0
  51. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/guards/drift_detection.py +0 -0
  52. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/guards/memory_budget.py +0 -0
  53. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/guards/memory_pressure.py +0 -0
  54. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/guards/self_healing.py +0 -0
  55. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/guards/tool_loop.py +0 -0
  56. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/hardware.py +0 -0
  57. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/ingress/__init__.py +0 -0
  58. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/ingress/app.py +0 -0
  59. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/ingress/dashboard_routes.py +0 -0
  60. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/ingress/launcher_routes.py +0 -0
  61. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/ingress/metrics_routes.py +0 -0
  62. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/ingress/openai_routes.py +0 -0
  63. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/language_tax.py +0 -0
  64. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/metrics/__init__.py +0 -0
  65. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/metrics/collector.py +0 -0
  66. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/metrics/prometheus.py +0 -0
  67. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/output_filters.py +0 -0
  68. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/plugins/__init__.py +0 -0
  69. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/plugins/base.py +0 -0
  70. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/plugins/loader.py +0 -0
  71. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/plugins/registry.py +0 -0
  72. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/routing/__init__.py +0 -0
  73. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/routing/adaptive.py +0 -0
  74. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/routing/auto_router.py +0 -0
  75. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/routing/budget.py +0 -0
  76. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/state/__init__.py +0 -0
  77. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/state/audit_log.py +0 -0
  78. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/state/replay.py +0 -0
  79. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/state/request_log.py +0 -0
  80. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/state/store.py +0 -0
  81. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/state/suggest_rules.py +0 -0
  82. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/token_estimation.py +0 -0
  83. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/token_estimation_accurate.py +0 -0
  84. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/translation/__init__.py +0 -0
  85. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/translation/anthropic.py +0 -0
  86. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/coderouter/translation/convert.py +0 -0
  87. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/README.md +0 -0
  88. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/assets/dashboard-demo.png +0 -0
  89. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
  90. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/backends/gguf_dl.md +0 -0
  91. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/backends/hf-ollama-models.md +0 -0
  92. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/backends/install-backends.en.md +0 -0
  93. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/backends/install-backends.md +0 -0
  94. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/backends/launcher-quickstart.md +0 -0
  95. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/backends/launcher.md +0 -0
  96. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/backends/llamacpp-direct.en.md +0 -0
  97. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/backends/llamacpp-direct.md +0 -0
  98. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/backends/lmstudio-direct.en.md +0 -0
  99. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/backends/lmstudio-direct.md +0 -0
  100. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/backends/verify-ollama-0.23.1.md +0 -0
  101. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/concepts/architecture.md +0 -0
  102. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/concepts/context-budget.md +0 -0
  103. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/concepts/continuous-probing.md +0 -0
  104. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/concepts/drift-detection.md +0 -0
  105. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/concepts/partial-stitch.md +0 -0
  106. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  107. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/designs/v1.6-auto-router-verification.md +0 -0
  108. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/designs/v1.6-auto-router.md +0 -0
  109. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/guides/free-tier-guide.en.md +0 -0
  110. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/guides/free-tier-guide.md +0 -0
  111. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/guides/language-tax.en.md +0 -0
  112. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/guides/language-tax.md +0 -0
  113. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/guides/security.en.md +0 -0
  114. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/guides/security.md +0 -0
  115. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/guides/troubleshooting.en.md +0 -0
  116. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/guides/troubleshooting.md +0 -0
  117. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/guides/usage-guide.en.md +0 -0
  118. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/guides/usage-guide.md +0 -0
  119. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/low-memory-integration.md +0 -0
  120. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/openrouter-roster/CHANGES.md +0 -0
  121. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/openrouter-roster/README.md +0 -0
  122. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/openrouter-roster/latest.json +0 -0
  123. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/retrospectives/v0.4.md +0 -0
  124. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/retrospectives/v0.5-verify.md +0 -0
  125. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/retrospectives/v0.5.md +0 -0
  126. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/retrospectives/v0.6.md +0 -0
  127. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/retrospectives/v0.7.md +0 -0
  128. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/retrospectives/v1.0-verify.md +0 -0
  129. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/retrospectives/v1.0.md +0 -0
  130. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/start/quickstart.en.md +0 -0
  131. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/start/quickstart.md +0 -0
  132. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/start/when-do-i-need-coderouter.en.md +0 -0
  133. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/docs/start/when-do-i-need-coderouter.md +0 -0
  134. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/examples/.env.example +0 -0
  135. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/examples/README.md +0 -0
  136. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/examples/providers-multiagent.yaml +0 -0
  137. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/examples/providers.context-budget-test.yaml +0 -0
  138. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/examples/providers.llamacpp-vllm.yaml +0 -0
  139. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/examples/providers.nvidia-nim.yaml +0 -0
  140. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/examples/providers.ollama-auto-custom.yaml +0 -0
  141. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/examples/providers.ollama-auto.yaml +0 -0
  142. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/examples/providers.ollama-free-chain.yaml +0 -0
  143. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/examples/providers.raspberrypi.yaml +0 -0
  144. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/examples/providers.yaml +0 -0
  145. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/scripts/demo_traffic.sh +0 -0
  146. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/scripts/openrouter_roster_diff.py +0 -0
  147. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/scripts/smoke_v2_2.sh +0 -0
  148. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/scripts/verify-providers.yaml +0 -0
  149. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/scripts/verify_ollama_0_23.py +0 -0
  150. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/scripts/verify_v0_5.sh +0 -0
  151. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/scripts/verify_v1_0.sh +0 -0
  152. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/__init__.py +0 -0
  153. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/conftest.py +0 -0
  154. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_adapter_anthropic.py +0 -0
  155. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_audit_log.py +0 -0
  156. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_auto_router.py +0 -0
  157. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_auto_router_cjk.py +0 -0
  158. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_backend_health.py +0 -0
  159. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_budget.py +0 -0
  160. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_capability.py +0 -0
  161. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_capability_registry.py +0 -0
  162. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_capability_registry_cache_control.py +0 -0
  163. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_claude_code_suitability.py +0 -0
  164. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_cli.py +0 -0
  165. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_cli_stats.py +0 -0
  166. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_config.py +0 -0
  167. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_context_budget.py +0 -0
  168. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_continuous_probe.py +0 -0
  169. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_dashboard_endpoint.py +0 -0
  170. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_doctor.py +0 -0
  171. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_doctor_apply.py +0 -0
  172. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_doctor_cache_probe.py +0 -0
  173. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_drift_actions.py +0 -0
  174. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_drift_detection.py +0 -0
  175. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_drift_detection_integration.py +0 -0
  176. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_env_file.py +0 -0
  177. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_env_security.py +0 -0
  178. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_errors.py +0 -0
  179. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_examples_yaml.py +0 -0
  180. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fallback.py +0 -0
  181. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fallback_anthropic.py +0 -0
  182. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fallback_cache_control.py +0 -0
  183. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fallback_cache_observed.py +0 -0
  184. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fallback_misconfig_warn.py +0 -0
  185. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fallback_paid_gate.py +0 -0
  186. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fallback_thinking.py +0 -0
  187. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fix_h1_prometheus_drift.py +0 -0
  188. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fix_h2_h6_translation.py +0 -0
  189. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fix_h3_client_reuse.py +0 -0
  190. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fix_h4_h5_guards.py +0 -0
  191. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fix_h7_probe_restore.py +0 -0
  192. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fix_h8_launcher_auth.py +0 -0
  193. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fix_mA_routing.py +0 -0
  194. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fix_mB_adapters.py +0 -0
  195. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fix_mC_convert.py +0 -0
  196. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fix_mD_logging_metrics.py +0 -0
  197. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fix_mE_config_validation.py +0 -0
  198. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_fix_mF_ingress.py +0 -0
  199. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_gguf_introspect.py +0 -0
  200. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_guards_tool_loop.py +0 -0
  201. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_hardware.py +0 -0
  202. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_ingress_anthropic.py +0 -0
  203. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_ingress_profile.py +0 -0
  204. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_language_tax.py +0 -0
  205. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_language_tax_integration.py +0 -0
  206. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_memory_budget.py +0 -0
  207. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_memory_pressure.py +0 -0
  208. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_metrics_cache.py +0 -0
  209. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_metrics_collector.py +0 -0
  210. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_metrics_cost.py +0 -0
  211. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_metrics_endpoint.py +0 -0
  212. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_metrics_jsonl.py +0 -0
  213. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_metrics_prometheus.py +0 -0
  214. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_metrics_prometheus_cache.py +0 -0
  215. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_openai_compat.py +0 -0
  216. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_openrouter_roster_diff.py +0 -0
  217. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_output_filters.py +0 -0
  218. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_output_filters_adapters.py +0 -0
  219. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_partial_stitch.py +0 -0
  220. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_plugins_integration.py +0 -0
  221. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_plugins_loader.py +0 -0
  222. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_plugins_registry.py +0 -0
  223. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_reasoning_strip.py +0 -0
  224. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_repair_byte_fallback.py +0 -0
  225. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_request_log.py +0 -0
  226. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_role_normalization.py +0 -0
  227. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_routing_adaptive.py +0 -0
  228. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_self_healing.py +0 -0
  229. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_setup_sh.py +0 -0
  230. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_state_store.py +0 -0
  231. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_token_estimation.py +0 -0
  232. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_token_estimation_accurate.py +0 -0
  233. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_tokens_saved_metric.py +0 -0
  234. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_tool_repair.py +0 -0
  235. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_translation_anthropic.py +0 -0
  236. {coderouter_cli-2.7.0 → coderouter_cli-2.7.2}/tests/test_translation_reverse.py +0 -0
@@ -93,3 +93,7 @@ docs/articles/
93
93
 
94
94
  # FUSE / virtiofs artifacts
95
95
  .fuse_hidden*
96
+
97
+ # tool-repair bench: locally generated results (curated evidence lives in results/)
98
+ benchmarks/tool-repair/results_*.json
99
+ benchmarks/tool-repair/results_*.md
@@ -6,6 +6,111 @@ versioning follows [SemVer](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [v2.7.2] — 2026-07-05 (R4 tool-call repair: nested-XML / JSON-envelope / call-syntax forms)
10
+
11
+ Single feature PR **#43**, driven by the 2026-07-05 L3 live benchmark
12
+ (6 local models × 2 wire paths × repairer-version A/B on an M3 Max). Closes
13
+ the three text-tool-call gap classes the bench measured in the wild. Fully
14
+ backward compatible: no config-schema changes, no signature changes,
15
+ no new dependencies (repairer stays stdlib-only).
16
+
17
+ ### Added
18
+
19
+ - **R4a — nested-XML name-attribute forms.** The tool name lives in a
20
+ `name` attribute rather than the tag itself:
21
+ `<tools><function name="echo" arguments='{...}'/></tools>`, bare
22
+ `<function .../>`, `<tool ... args='{...}'/>`. Fixed known tag set;
23
+ `name` must be allow-listed; attribute values are delegated to the R1
24
+ lenient-JSON pipe. (PR #43)
25
+ - **R4b — JSON envelope forms.** Models sometimes echo the response wrapper
26
+ verbatim into the text body: `{"tool_calls": [...]}` and legacy
27
+ `{"function_call": {"name": ..., "arguments": "<JSON string>"}}` (string
28
+ arguments double-parsed). All-or-nothing allow-list validation: one
29
+ non-allow-listed inner call rejects the whole envelope. (PR #43)
30
+ - **R4c — call-syntax family.** One extractor for the "name + parens + args"
31
+ idiom observed independently across three model families (Gemma, Mistral,
32
+ phi4): `print(default_api.echo(message="probe"))`, `echo(message="probe")`,
33
+ `echo(message: 'demo')` (colon kwargs), `write_note({JSON})`. Recognised
34
+ only inside a fence or on a standalone line, never inline in prose. The
35
+ argument list must parse completely — a corrupted inner JSON is left
36
+ alone rather than executed with broken args. (PR #43)
37
+ - **50 new unit tests** (`tests/test_tool_repair_r4.py`) and **29 new bench
38
+ corpus cases** (55 → 84), including 10 adversarial-review counterexamples
39
+ locked in as negatives (blank-line cue variants among them). (PR #43)
40
+
41
+ ### Changed
42
+
43
+ - **Prose-cue false-positive guard hardened** after two adversarial review
44
+ rounds: expanded cue vocabulary (`as follows` / `convention` / `format` /
45
+ `sample` / `payload` / ...), 200-char window with whole-previous-line
46
+ matching (two blank-line window bugs fixed), and a colon-ended lead-in
47
+ rule (full-width `:` U+FF1A included). The colon rule deliberately
48
+ over-suppresses genuine "I'll call it now:" lead-ins — FP-zero-first is
49
+ documented in the module docstring as a design decision. (PR #43)
50
+
51
+ ### Results
52
+
53
+ - L1 offline bench: recall **78.0% → 100%** (59/59), false positives
54
+ **0/25** maintained, existing 70 cases regression-free.
55
+ - L2/L3 live (temperature 0, 100 requests per path, zero errors):
56
+ mistral:7b via router **80% → 100%** (third 0→100 model after
57
+ qwen2.5-coder 1.5b/7b); phi4-mini **40% → 80%**, with the remaining 20%
58
+ being exactly the semantically-corrupted case the repairer refuses by
59
+ design.
60
+
61
+ ---
62
+
63
+ ## [v2.7.1] — 2026-07-04 (Native-endpoint shims + benchmarked tool-repair upgrade)
64
+
65
+ Two feature PRs driven by the 2026-07-04 competitive re-survey and the new
66
+ tool-call repair benchmark: **PR #39** (native `/v1/messages` endpoint gap
67
+ shims) and **PR #40** (lenient tool-call repair). All new behavior is
68
+ **opt-in with `off` defaults** (except the previously-404 `count_tokens`
69
+ endpoint, which is additive), no config-schema breaking changes, and the
70
+ Core runtime stays at **5 dependencies**.
71
+
72
+ ### Added
73
+
74
+ - **`POST /v1/messages/count_tokens`** (was 404). Anthropic-shape
75
+ `{"input_tokens": N}`; accurate when the provider declares
76
+ `tokenizer_path`, char/4 heuristic otherwise. Fills a gap native backends
77
+ leave open (Ollama's Anthropic-compatible API ships without
78
+ `count_tokens` / `tool_choice` / prompt caching). (PR #39)
79
+ - **`tool_choice_action: off | warn | emulate`** (profile option) +
80
+ `Capabilities.tool_choice` + registry field + `provider_supports_tool_choice()`.
81
+ `emulate` rewrites forced tool_choice (`any` / `tool`) into a system-prompt
82
+ instruction for non-supporting backends; the original request is preserved
83
+ for fallback to capable providers. New capability-degraded reason:
84
+ `unsupported-backend`. (PR #39)
85
+ - **`cache_control_action: off | strip`** (profile option). `strip` removes
86
+ `cache_control` markers before sending to non-supporting providers
87
+ (logged as `cache-control-stripped`; deliberately no tokens-saved
88
+ accounting). (PR #39)
89
+ - **Lenient tool-call repair** (`translation/tool_repair.py`): second-pass
90
+ parsing for malformed JSON (double braces, trailing commas, single quotes,
91
+ unquoted keys), key aliases (`tool`/`tool_name`, `parameters`/`input`/`args`),
92
+ and XML-flavoured forms — all gated on `allowed_tool_names`. Residue
93
+ cleanup (empty fences, `[,]`). Benchmark: recall 80.6% → **100%** on a
94
+ 55-case corpus; live fail-rate at default temperature 17% → **1%**
95
+ (qwen2.5-coder:7b × 100 requests). (PR #40)
96
+
97
+ ### Fixed
98
+
99
+ - **Two real false positives in the previous repairer** (python-fence dict
100
+ literals and documentation JSON with allowed tool names were converted
101
+ into executable calls — same class as the v2.7.0 code-eating regression).
102
+ Now fixed and gated by an expanded 12-case negative corpus. (PR #40)
103
+
104
+ ### Docs
105
+
106
+ - README repositioned for the native `/v1/messages` era: repair + guards
107
+ lead, wire translation demoted to last (passthrough note); new section
108
+ "why a router when Ollama connects directly?".
109
+
110
+ Tests: **1481 passed, 0 failed** (1401 → 1481; +37 shim, +43 lenient-repair).
111
+
112
+ ---
113
+
9
114
  ## [v2.7.0] — 2026-07-02 (Reliability & security: full-source review fixes)
10
115
 
11
116
  22 reliability and security fixes from a full-source review (26,600 lines),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.7.0
3
+ Version: 2.7.2
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
@@ -70,8 +70,8 @@ Description-Content-Type: text/markdown
70
70
 
71
71
 
72
72
  ┌─ CodeRouter ─┐
73
- 翻訳 + 修復 │──→ ① ローカル (Ollama — 無料・最速)
74
- ガード + 監視 │──→ ② 無料クラウド (OpenRouter / NIM)
73
+ 修復 + ガード │──→ ① ローカル (Ollama — 無料・最速)
74
+ 監視 + 診断 │──→ ② 無料クラウド (OpenRouter / NIM)
75
75
  │ 自動フォールバック │──→ ③ 有料 (Claude — opt-in 時のみ)
76
76
  └──────────────┘
77
77
  ```
@@ -79,13 +79,31 @@ Description-Content-Type: text/markdown
79
79
  **やってくれること:**
80
80
 
81
81
  - ローカルモデルが壊した tool calling を Claude Code に届く前に修復する
82
+ - 8 時間回しても止まらないように 6 種類のガードで守る
82
83
  - 1 つ目が落ちたら自動で次のプロバイダに切り替える
83
84
  - 有料 API は明示的に許可したときだけ使う (デフォルトは無料のみ)
84
- - 8 時間回しても止まらないように 6 種類のガードで守る
85
85
  - 何がおかしいか `coderouter doctor` コマンド一発で診断する
86
86
 
87
87
  ---
88
88
 
89
+ ## 「Ollama に直結できるのに、なぜルーター?」
90
+
91
+ 2026 年、Ollama (v0.14+) / LM Studio (0.4.1+) / llama.cpp / vLLM は Anthropic 互換 `/v1/messages` を標準装備しました。`ANTHROPIC_BASE_URL` を直接向ければ Claude Code は一応動きます。
92
+
93
+ **でも直結では、こうなります:**
94
+
95
+ | 直結の現実 | CodeRouter 経由 |
96
+ |---|---|
97
+ | 壊れた tool call は壊れたまま届く | 届く前に修復 |
98
+ | backend が落ちたらセッション終了 | ローカル → 無料 → 有料へ自動フォールバック |
99
+ | 長時間で context 溢れ・drift・ループ | 6 系統ガード + self-healing |
100
+ | モデル名がハードコード (リタイアで即エラー) | プロファイルで抽象化、差し替え 1 行 |
101
+ | 何が悪いか分からない | `doctor` 6 プローブ + `/dashboard` + audit/replay |
102
+
103
+ 直結で困っていないなら CodeRouter は不要です。**長時間・無人・弱いモデル**のどれかに当てはまったら、戻ってきてください。
104
+
105
+ ---
106
+
89
107
  ## インストール (3 行)
90
108
 
91
109
  ```bash
@@ -120,8 +138,9 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
120
138
 
121
139
  | あなたの状況 | CodeRouter は? |
122
140
  |---|---|
123
- | Claude Code + ローカル Ollama で tool calling が壊れる | **必須** — wire 変換 + tool 修復 |
124
- | Claude Code + ローカルで長時間回すと止まる | **便利** — 6 系統ガード + self-healing |
141
+ | Claude Code + ローカル Ollama で tool calling が壊れる | **必須** — tool 修復 (+ 必要なら wire 変換) |
142
+ | Claude Code + ローカルで長時間回すと止まる | **必須級** — 6 系統ガード + self-healing |
143
+ | Ollama v0.14+ / LM Studio にネイティブ直結で動いてる | **便利** — 直結に無い fallback / ガード / 診断を追加 (passthrough で翻訳ゼロ) |
125
144
  | codex / gemini-cli + Ollama 直繋ぎで動いてる | オプション — フォールバックが欲しいなら |
126
145
  | Claude API を直接叩いてて問題ない | 不要 |
127
146
 
@@ -131,14 +150,14 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
131
150
 
132
151
  ## 主な機能
133
152
 
134
- ### 接続と修復
153
+ ### 修復と接続
135
154
 
136
155
  | 機能 | 何をしてくれるか |
137
156
  |---|---|
138
- | **Wire 翻訳** | Claude Code (Anthropic形式) ↔ Ollama (OpenAI形式) を自動変換 |
139
157
  | **Tool-call 修復** | ローカルモデルがテキストで吐いた JSON を正しい tool_use ブロックに復元 |
140
158
  | **3 層フォールバック** | ローカル → 無料クラウド → 有料の順に自動切替 |
141
- | **出力フィルタ** | `<think>` タグ漏れ、stop marker 漏れを自動除去 |
159
+ | **出力フィルタ** | `<think>` タグ漏れ、stop marker 漏れ、byte-fallback (`<0xNN>`) を自動除去/修復 |
160
+ | **Wire 翻訳** | Anthropic 形式 ↔ OpenAI 形式を自動変換 (ネイティブ `/v1/messages` 対応 backend は passthrough で翻訳ゼロ) |
142
161
 
143
162
  ### 長時間運用ガード
144
163
 
@@ -25,8 +25,8 @@
25
25
 
26
26
 
27
27
  ┌─ CodeRouter ─┐
28
- 翻訳 + 修復 │──→ ① ローカル (Ollama — 無料・最速)
29
- ガード + 監視 │──→ ② 無料クラウド (OpenRouter / NIM)
28
+ 修復 + ガード │──→ ① ローカル (Ollama — 無料・最速)
29
+ 監視 + 診断 │──→ ② 無料クラウド (OpenRouter / NIM)
30
30
  │ 自動フォールバック │──→ ③ 有料 (Claude — opt-in 時のみ)
31
31
  └──────────────┘
32
32
  ```
@@ -34,13 +34,31 @@
34
34
  **やってくれること:**
35
35
 
36
36
  - ローカルモデルが壊した tool calling を Claude Code に届く前に修復する
37
+ - 8 時間回しても止まらないように 6 種類のガードで守る
37
38
  - 1 つ目が落ちたら自動で次のプロバイダに切り替える
38
39
  - 有料 API は明示的に許可したときだけ使う (デフォルトは無料のみ)
39
- - 8 時間回しても止まらないように 6 種類のガードで守る
40
40
  - 何がおかしいか `coderouter doctor` コマンド一発で診断する
41
41
 
42
42
  ---
43
43
 
44
+ ## 「Ollama に直結できるのに、なぜルーター?」
45
+
46
+ 2026 年、Ollama (v0.14+) / LM Studio (0.4.1+) / llama.cpp / vLLM は Anthropic 互換 `/v1/messages` を標準装備しました。`ANTHROPIC_BASE_URL` を直接向ければ Claude Code は一応動きます。
47
+
48
+ **でも直結では、こうなります:**
49
+
50
+ | 直結の現実 | CodeRouter 経由 |
51
+ |---|---|
52
+ | 壊れた tool call は壊れたまま届く | 届く前に修復 |
53
+ | backend が落ちたらセッション終了 | ローカル → 無料 → 有料へ自動フォールバック |
54
+ | 長時間で context 溢れ・drift・ループ | 6 系統ガード + self-healing |
55
+ | モデル名がハードコード (リタイアで即エラー) | プロファイルで抽象化、差し替え 1 行 |
56
+ | 何が悪いか分からない | `doctor` 6 プローブ + `/dashboard` + audit/replay |
57
+
58
+ 直結で困っていないなら CodeRouter は不要です。**長時間・無人・弱いモデル**のどれかに当てはまったら、戻ってきてください。
59
+
60
+ ---
61
+
44
62
  ## インストール (3 行)
45
63
 
46
64
  ```bash
@@ -75,8 +93,9 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
75
93
 
76
94
  | あなたの状況 | CodeRouter は? |
77
95
  |---|---|
78
- | Claude Code + ローカル Ollama で tool calling が壊れる | **必須** — wire 変換 + tool 修復 |
79
- | Claude Code + ローカルで長時間回すと止まる | **便利** — 6 系統ガード + self-healing |
96
+ | Claude Code + ローカル Ollama で tool calling が壊れる | **必須** — tool 修復 (+ 必要なら wire 変換) |
97
+ | Claude Code + ローカルで長時間回すと止まる | **必須級** — 6 系統ガード + self-healing |
98
+ | Ollama v0.14+ / LM Studio にネイティブ直結で動いてる | **便利** — 直結に無い fallback / ガード / 診断を追加 (passthrough で翻訳ゼロ) |
80
99
  | codex / gemini-cli + Ollama 直繋ぎで動いてる | オプション — フォールバックが欲しいなら |
81
100
  | Claude API を直接叩いてて問題ない | 不要 |
82
101
 
@@ -86,14 +105,14 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
86
105
 
87
106
  ## 主な機能
88
107
 
89
- ### 接続と修復
108
+ ### 修復と接続
90
109
 
91
110
  | 機能 | 何をしてくれるか |
92
111
  |---|---|
93
- | **Wire 翻訳** | Claude Code (Anthropic形式) ↔ Ollama (OpenAI形式) を自動変換 |
94
112
  | **Tool-call 修復** | ローカルモデルがテキストで吐いた JSON を正しい tool_use ブロックに復元 |
95
113
  | **3 層フォールバック** | ローカル → 無料クラウド → 有料の順に自動切替 |
96
- | **出力フィルタ** | `<think>` タグ漏れ、stop marker 漏れを自動除去 |
114
+ | **出力フィルタ** | `<think>` タグ漏れ、stop marker 漏れ、byte-fallback (`<0xNN>`) を自動除去/修復 |
115
+ | **Wire 翻訳** | Anthropic 形式 ↔ OpenAI 形式を自動変換 (ネイティブ `/v1/messages` 対応 backend は passthrough で翻訳ゼロ) |
97
116
 
98
117
  ### 長時間運用ガード
99
118
 
@@ -115,6 +115,20 @@ class RegistryCapabilities(BaseModel):
115
115
  "startup check)."
116
116
  ),
117
117
  )
118
+ tool_choice: bool | None = Field(
119
+ default=None,
120
+ description=(
121
+ "S2 (shim): does the upstream honor a forced Anthropic "
122
+ "``tool_choice`` (``{type: any}`` / ``{type: tool}``) on the "
123
+ "wire? When True, the fallback engine's ``tool_choice_action`` "
124
+ "leaves the request untouched. ``None`` (default) = no opinion "
125
+ "→ the gate falls back to ``provider.kind == 'anthropic'``. "
126
+ "``False`` hard-disables even on an anthropic-kind provider "
127
+ "(useful when a specific model regresses). Independent from "
128
+ "``providers.yaml capabilities.tool_choice`` (explicit "
129
+ "per-provider opt-in, highest precedence)."
130
+ ),
131
+ )
118
132
  cache_control: bool | None = Field(
119
133
  default=None,
120
134
  description=(
@@ -201,6 +215,7 @@ class ResolvedCapabilities:
201
215
  max_context_tokens: int | None = None
202
216
  claude_code_suitability: Literal["ok", "degraded"] | None = None
203
217
  cache_control: bool | None = None
218
+ tool_choice: bool | None = None
204
219
 
205
220
 
206
221
  # ---------------------------------------------------------------------------
@@ -253,6 +268,7 @@ class CapabilityRegistry:
253
268
  resolved_max_ctx: int | None = None
254
269
  resolved_suitability: Literal["ok", "degraded"] | None = None
255
270
  resolved_cache_control: bool | None = None
271
+ resolved_tool_choice: bool | None = None
256
272
 
257
273
  thinking_locked = False
258
274
  reasoning_locked = False
@@ -260,6 +276,7 @@ class CapabilityRegistry:
260
276
  max_ctx_locked = False
261
277
  suitability_locked = False
262
278
  cache_control_locked = False
279
+ tool_choice_locked = False
263
280
 
264
281
  for rule in self._rules:
265
282
  if not rule.kind_matches(kind):
@@ -285,6 +302,9 @@ class CapabilityRegistry:
285
302
  if not cache_control_locked and caps.cache_control is not None:
286
303
  resolved_cache_control = caps.cache_control
287
304
  cache_control_locked = True
305
+ if not tool_choice_locked and caps.tool_choice is not None:
306
+ resolved_tool_choice = caps.tool_choice
307
+ tool_choice_locked = True
288
308
  if (
289
309
  thinking_locked
290
310
  and reasoning_locked
@@ -292,6 +312,7 @@ class CapabilityRegistry:
292
312
  and max_ctx_locked
293
313
  and suitability_locked
294
314
  and cache_control_locked
315
+ and tool_choice_locked
295
316
  ):
296
317
  break
297
318
 
@@ -302,6 +323,7 @@ class CapabilityRegistry:
302
323
  max_context_tokens=resolved_max_ctx,
303
324
  claude_code_suitability=resolved_suitability,
304
325
  cache_control=resolved_cache_control,
326
+ tool_choice=resolved_tool_choice,
305
327
  )
306
328
 
307
329
  # ------------------------------------------------------------------
@@ -43,6 +43,18 @@ class Capabilities(BaseModel):
43
43
  # you explicitly want the raw reasoning text to flow to the client
44
44
  # (e.g. CodeRouter is fronting a reasoning-aware downstream).
45
45
  reasoning_passthrough: bool = False
46
+ # S2 (shim): Anthropic's ``tool_choice`` forcing modes (``{type: any}``
47
+ # / ``{type: tool, name: ...}``). Only a subset of backends honor a
48
+ # forced tool_choice on the wire; openai_compat translation drops it.
49
+ # This narrow per-model flag mirrors ``thinking`` / ``prompt_cache``:
50
+ # when unset (None), the capability gate falls back to the registry and
51
+ # then to a ``kind == "anthropic"`` heuristic (see
52
+ # ``coderouter/routing/capability.py``). Motivation: let the fallback
53
+ # engine's ``tool_choice_action`` emulate forced tool calls via a
54
+ # system-prompt directive on backends that would otherwise silently
55
+ # ignore the field. Backward compatible — None leaves v2.x behavior
56
+ # untouched (no gate, no emulation).
57
+ tool_choice: bool | None = None
46
58
  # v1.0+ fields, declared early so providers.yaml can future-proof
47
59
  reasoning_control: Literal["none", "openai", "anthropic", "provider_specific"] = "none"
48
60
  mcp: Literal["none", "anthropic", "provider_specific"] = "none"
@@ -706,6 +718,72 @@ class FallbackChain(BaseModel):
706
718
  ),
707
719
  )
708
720
 
721
+ # --- S2 (shim): tool_choice capability gate + emulation -----------------
722
+ #
723
+ # Anthropic clients (Claude Code, SDKs) can pin the model to a specific
724
+ # tool with ``tool_choice: {type: "tool", name: "X"}`` or force *some*
725
+ # tool with ``{type: "any"}``. Native Anthropic honors this; most
726
+ # openai_compat backends silently ignore it after translation, so the
727
+ # model may answer with plain text where the client expected a tool
728
+ # call. This knob decides what the fallback engine does when a forced
729
+ # tool_choice request is routed to a provider that does not support it
730
+ # (per ``provider_supports_tool_choice``):
731
+ #
732
+ # * ``off`` — no detection / no mutation / no log. Backward-compat
733
+ # default (identical to pre-shim behavior).
734
+ # * ``warn`` — emit a ``capability-degraded`` log line only; the
735
+ # request is sent unchanged.
736
+ # * ``emulate`` — strip the ``tool_choice`` field and inject an English
737
+ # directive into the system prompt instructing the
738
+ # model to call the requested tool. Best-effort
739
+ # forcing for backends without native support. The
740
+ # original request object is left untouched so a later
741
+ # capable provider in the chain still receives the real
742
+ # ``tool_choice``.
743
+ tool_choice_action: Literal["off", "warn", "emulate"] = Field(
744
+ default="off",
745
+ description=(
746
+ "S2 (shim): action when a request carries a forced "
747
+ "``tool_choice`` (``{type: any}`` or ``{type: tool}``) and the "
748
+ "target provider does not support it. ``off`` (default) leaves "
749
+ "the request unchanged. ``warn`` emits a ``capability-degraded`` "
750
+ "log only. ``emulate`` strips ``tool_choice`` and injects a "
751
+ "system-prompt directive to coax the model into calling the "
752
+ "tool. Per-provider — capable providers are never mutated."
753
+ ),
754
+ )
755
+
756
+ # --- S3 (shim): cache_control strip -------------------------------------
757
+ #
758
+ # By default cache_control markers are simply lost during Anthropic →
759
+ # OpenAI translation and the ``capability-degraded`` gate logs it
760
+ # (v0.5-B, observability only). Some strict openai_compat backends,
761
+ # however, 400 when an unexpected ``cache_control`` key rides along on a
762
+ # content block (rather than silently ignoring it). ``strip`` proactively
763
+ # removes those keys from a deep copy of the request before dispatch to
764
+ # a non-supporting provider, so the marker never reaches the wire.
765
+ #
766
+ # * ``off`` — legacy behavior: leave the request as-is and rely on
767
+ # the existing ``capability-degraded`` observability log.
768
+ # * ``strip`` — deep-copy the request and remove every ``cache_control``
769
+ # key from system / tools / message blocks before sending
770
+ # to a non-supporting provider; emit a
771
+ # ``cache-control-stripped`` log with the marker count.
772
+ # Does NOT emit a tokens-saved event (this is not token
773
+ # savings, just a wire-compatibility strip).
774
+ cache_control_action: Literal["off", "strip"] = Field(
775
+ default="off",
776
+ description=(
777
+ "S3 (shim): action when a request carries ``cache_control`` "
778
+ "markers and the target provider does not support them. ``off`` "
779
+ "(default) keeps legacy behavior (marker dropped in translation, "
780
+ "``capability-degraded`` log only). ``strip`` removes the "
781
+ "``cache_control`` keys from a deep copy before dispatch and "
782
+ "emits a ``cache-control-stripped`` log. Per-provider — capable "
783
+ "providers are never mutated."
784
+ ),
785
+ )
786
+
709
787
  @model_validator(mode="after")
710
788
  def _check_context_budget_thresholds_ordered(self) -> FallbackChain:
711
789
  """v2.0-F (mE): cross-field sanity of the context-budget ratios.
@@ -38,6 +38,8 @@ from coderouter.routing import (
38
38
  NoProvidersAvailableError,
39
39
  )
40
40
  from coderouter.routing.auto_router import RESERVED_PROFILE_NAME, classify
41
+ from coderouter.token_estimation import extract_text_from_anthropic_request
42
+ from coderouter.token_estimation_accurate import count_tokens, is_accuracy_available
41
43
  from coderouter.translation import (
42
44
  AnthropicRequest,
43
45
  AnthropicStreamEvent,
@@ -316,6 +318,112 @@ async def messages(
316
318
  return anth_resp.model_dump(exclude_none=True)
317
319
 
318
320
 
321
+ def _resolve_count_tokens_tokenizer_path(
322
+ config: Any, profile: str | None
323
+ ) -> str | None:
324
+ """S1 (shim): best-effort resolve a local tokenizer.json for counting.
325
+
326
+ Routing for ``count_tokens`` is not a full chain resolution — we only
327
+ need *a* representative tokenizer for the profile the request would use.
328
+ We take the first provider of the resolved profile (or the default
329
+ profile) and return its ``tokenizer_path`` when declared. Any resolution
330
+ hiccup (unknown profile, empty chain, stub config in tests) returns
331
+ ``None``, which makes :func:`count_tokens` fall back to the char/4
332
+ heuristic — a graceful degrade rather than an error.
333
+ """
334
+ try:
335
+ chosen = profile or config.default_profile
336
+ chain_cfg = config.profile_by_name(chosen)
337
+ for pname in getattr(chain_cfg, "providers", []) or []:
338
+ pconf = next((p for p in config.providers if p.name == pname), None)
339
+ if pconf is not None:
340
+ tok = getattr(pconf, "tokenizer_path", None)
341
+ if tok:
342
+ return tok
343
+ # First provider resolved but declares no tokenizer — stop
344
+ # here (don't scan the whole chain for an unrelated one).
345
+ return None
346
+ except (AttributeError, KeyError, ValueError, TypeError):
347
+ return None
348
+ return None
349
+
350
+
351
+ @router.post("/messages/count_tokens", response_model=None)
352
+ async def count_tokens_route(
353
+ payload: dict[str, Any],
354
+ request: Request,
355
+ x_coderouter_profile: str | None = Header(default=None, alias=_PROFILE_HEADER),
356
+ x_coderouter_mode: str | None = Header(default=None, alias=_MODE_HEADER),
357
+ ) -> dict[str, Any]:
358
+ """Anthropic ``POST /v1/messages/count_tokens`` — local token estimate.
359
+
360
+ Mirrors Anthropic's count_tokens endpoint: the request body has the
361
+ same shape as ``/v1/messages`` (minus the ``max_tokens`` requirement)
362
+ and the response is ``{"input_tokens": N}``. CodeRouter answers this
363
+ entirely locally — there is no upstream round-trip — using the same
364
+ text-extraction the language-tax / context-budget guards use, feeding
365
+ an accurate local tokenizer when the routed provider declares one and
366
+ the ``accuracy`` extra is installed, otherwise the char/4 heuristic.
367
+
368
+ Validation is deliberately looser than :class:`AnthropicRequest` (no
369
+ ``max_tokens`` needed): ``model`` and a non-empty ``messages`` list are
370
+ required, everything else optional. Profile selection follows the same
371
+ body > profile-header > mode-header precedence as ``/messages`` so the
372
+ tokenizer resolves against the provider the real request would hit.
373
+ """
374
+ config = request.app.state.config
375
+
376
+ if not isinstance(payload, dict):
377
+ raise HTTPException(status_code=400, detail="request body must be a JSON object")
378
+
379
+ messages = payload.get("messages")
380
+ if not isinstance(messages, list) or not messages:
381
+ raise HTTPException(
382
+ status_code=400,
383
+ detail="'messages' is required and must be a non-empty list",
384
+ )
385
+ if "model" not in payload:
386
+ raise HTTPException(status_code=400, detail="'model' is required")
387
+
388
+ # Profile selection — body field wins over header, mode header last
389
+ # (same policy as /messages). Kept minimal: we only need the profile to
390
+ # pick a representative tokenizer, so an unknown mode/profile degrades
391
+ # to the default rather than 400'ing the count.
392
+ profile = payload.get("profile") or x_coderouter_profile
393
+ if profile is None and x_coderouter_mode:
394
+ try:
395
+ profile = config.resolve_mode(x_coderouter_mode)
396
+ except (KeyError, AttributeError):
397
+ profile = None
398
+
399
+ # Combine system + messages (+ tool JSON length) into one text blob and
400
+ # count. tools contribute their JSON length as a coarse proxy for the
401
+ # schema tokens Anthropic would bill.
402
+ text = extract_text_from_anthropic_request(
403
+ system=payload.get("system"),
404
+ messages=messages,
405
+ )
406
+ tools = payload.get("tools")
407
+ if isinstance(tools, list) and tools:
408
+ with contextlib.suppress(TypeError, ValueError):
409
+ text = f"{text}\n{json.dumps(tools, ensure_ascii=False)}"
410
+
411
+ tokenizer_path = _resolve_count_tokens_tokenizer_path(config, profile)
412
+ input_tokens = count_tokens(text, tokenizer_path=tokenizer_path)
413
+ # Report the method count_tokens *actually* used: the accurate backend
414
+ # only engages when a path is declared AND the optional ``tokenizers``
415
+ # dependency is importable (see token_estimation_accurate). Otherwise
416
+ # count_tokens transparently falls back to char/4 — so we must too, or
417
+ # the log would misreport a heuristic count as tokenizer-accurate.
418
+ method = "tokenizer" if (tokenizer_path and is_accuracy_available()) else "heuristic"
419
+
420
+ logger.info(
421
+ "count-tokens-served",
422
+ extra={"method": method, "input_tokens": input_tokens},
423
+ )
424
+ return {"input_tokens": input_tokens}
425
+
426
+
319
427
  async def _anthropic_sse_iterator(
320
428
  engine: FallbackEngine, anth_req: AnthropicRequest
321
429
  ) -> AsyncIterator[str]:
@@ -129,6 +129,7 @@ CapabilityDegradedReason = Literal[
129
129
  "provider-does-not-support",
130
130
  "translation-lossy",
131
131
  "non-standard-field",
132
+ "unsupported-backend",
132
133
  ]
133
134
  """Why a capability was degraded.
134
135
 
@@ -142,6 +143,10 @@ CapabilityDegradedReason = Literal[
142
143
  - ``non-standard-field``: upstream emits a field that is not in the spec
143
144
  the ingress speaks, so we strip it on the response-side boundary.
144
145
  v0.5-C reasoning field.
146
+ - ``unsupported-backend``: the request asks for a semantic the target
147
+ backend does not honor (S2 forced ``tool_choice``); with action
148
+ ``warn`` the request is sent unchanged, with ``emulate`` a separate
149
+ ``tool-choice-emulated`` line records the substituted directive.
145
150
  """
146
151
 
147
152