coderouter-cli 2.6.1__tar.gz → 2.7.1__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 (234) hide show
  1. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/CHANGELOG.md +188 -0
  2. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/PKG-INFO +28 -9
  3. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/README.md +27 -8
  4. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/adapters/anthropic_native.py +66 -30
  5. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/adapters/base.py +46 -3
  6. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/adapters/openai_compat.py +53 -11
  7. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/config/capability_registry.py +22 -0
  8. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/config/schemas.py +232 -0
  9. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/context_budget.py +114 -19
  10. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/continuous_probe.py +9 -1
  11. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/memory_pressure.py +16 -2
  12. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/self_healing.py +14 -0
  13. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/anthropic_routes.py +223 -1
  14. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/app.py +162 -1
  15. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/launcher_routes.py +187 -11
  16. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/openai_routes.py +77 -7
  17. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/logging.py +29 -3
  18. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/metrics/collector.py +454 -245
  19. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/metrics/prometheus.py +3 -3
  20. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/routing/capability.py +217 -0
  21. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/routing/fallback.py +539 -27
  22. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/state/audit_log.py +69 -7
  23. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/state/request_log.py +70 -7
  24. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/translation/convert.py +112 -9
  25. coderouter_cli-2.7.1/coderouter/translation/tool_repair.py +898 -0
  26. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/launcher.md +2 -0
  27. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/concepts/architecture.md +1 -1
  28. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/concepts/drift-detection.md +3 -2
  29. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/security.en.md +17 -3
  30. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/security.md +3 -2
  31. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/pyproject.toml +1 -1
  32. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_auto_router.py +9 -11
  33. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_capability_degraded_payload.py +6 -4
  34. coderouter_cli-2.7.1/tests/test_capability_tool_choice.py +251 -0
  35. coderouter_cli-2.7.1/tests/test_config_shim_actions.py +49 -0
  36. coderouter_cli-2.7.1/tests/test_fallback_cache_strip.py +254 -0
  37. coderouter_cli-2.7.1/tests/test_fallback_tool_choice_emulate.py +267 -0
  38. coderouter_cli-2.7.1/tests/test_fix_h1_prometheus_drift.py +102 -0
  39. coderouter_cli-2.7.1/tests/test_fix_h2_h6_translation.py +114 -0
  40. coderouter_cli-2.7.1/tests/test_fix_h3_client_reuse.py +188 -0
  41. coderouter_cli-2.7.1/tests/test_fix_h4_h5_guards.py +289 -0
  42. coderouter_cli-2.7.1/tests/test_fix_h7_probe_restore.py +152 -0
  43. coderouter_cli-2.7.1/tests/test_fix_h8_launcher_auth.py +316 -0
  44. coderouter_cli-2.7.1/tests/test_fix_mA_routing.py +616 -0
  45. coderouter_cli-2.7.1/tests/test_fix_mB_adapters.py +215 -0
  46. coderouter_cli-2.7.1/tests/test_fix_mC_convert.py +494 -0
  47. coderouter_cli-2.7.1/tests/test_fix_mD_logging_metrics.py +410 -0
  48. coderouter_cli-2.7.1/tests/test_fix_mE_config_validation.py +260 -0
  49. coderouter_cli-2.7.1/tests/test_fix_mF_ingress.py +349 -0
  50. coderouter_cli-2.7.1/tests/test_ingress_count_tokens.py +150 -0
  51. coderouter_cli-2.7.1/tests/test_tool_repair_lenient.py +388 -0
  52. coderouter_cli-2.6.1/coderouter/translation/tool_repair.py +0 -277
  53. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/.gitignore +0 -0
  54. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/LICENSE +0 -0
  55. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/README.en.md +0 -0
  56. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/__init__.py +0 -0
  57. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/__main__.py +0 -0
  58. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/adapters/__init__.py +0 -0
  59. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/adapters/registry.py +0 -0
  60. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/cli.py +0 -0
  61. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/cli_stats.py +0 -0
  62. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/config/__init__.py +0 -0
  63. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/config/env_file.py +0 -0
  64. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/config/loader.py +0 -0
  65. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/cost.py +0 -0
  66. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/data/__init__.py +0 -0
  67. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/data/model-capabilities.yaml +0 -0
  68. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/doctor.py +0 -0
  69. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/doctor_apply.py +0 -0
  70. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/env_security.py +0 -0
  71. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/errors.py +0 -0
  72. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/gguf_introspect.py +0 -0
  73. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/__init__.py +0 -0
  74. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/_fingerprint.py +0 -0
  75. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/backend_health.py +0 -0
  76. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/drift_actions.py +0 -0
  77. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/drift_detection.py +0 -0
  78. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/memory_budget.py +0 -0
  79. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/tool_loop.py +0 -0
  80. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/hardware.py +0 -0
  81. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/__init__.py +0 -0
  82. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/dashboard_routes.py +0 -0
  83. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/metrics_routes.py +0 -0
  84. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/language_tax.py +0 -0
  85. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/metrics/__init__.py +0 -0
  86. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/output_filters.py +0 -0
  87. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/plugins/__init__.py +0 -0
  88. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/plugins/base.py +0 -0
  89. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/plugins/loader.py +0 -0
  90. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/plugins/registry.py +0 -0
  91. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/routing/__init__.py +0 -0
  92. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/routing/adaptive.py +0 -0
  93. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/routing/auto_router.py +0 -0
  94. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/routing/budget.py +0 -0
  95. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/state/__init__.py +0 -0
  96. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/state/replay.py +0 -0
  97. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/state/store.py +0 -0
  98. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/state/suggest_rules.py +0 -0
  99. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/token_estimation.py +0 -0
  100. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/token_estimation_accurate.py +0 -0
  101. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/translation/__init__.py +0 -0
  102. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/translation/anthropic.py +0 -0
  103. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/README.md +0 -0
  104. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/assets/dashboard-demo.png +0 -0
  105. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
  106. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/gguf_dl.md +0 -0
  107. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/hf-ollama-models.md +0 -0
  108. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/install-backends.en.md +0 -0
  109. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/install-backends.md +0 -0
  110. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/launcher-quickstart.md +0 -0
  111. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/llamacpp-direct.en.md +0 -0
  112. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/llamacpp-direct.md +0 -0
  113. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/lmstudio-direct.en.md +0 -0
  114. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/lmstudio-direct.md +0 -0
  115. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/verify-ollama-0.23.1.md +0 -0
  116. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/concepts/context-budget.md +0 -0
  117. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/concepts/continuous-probing.md +0 -0
  118. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/concepts/partial-stitch.md +0 -0
  119. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  120. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/designs/v1.6-auto-router-verification.md +0 -0
  121. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/designs/v1.6-auto-router.md +0 -0
  122. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/free-tier-guide.en.md +0 -0
  123. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/free-tier-guide.md +0 -0
  124. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/language-tax.en.md +0 -0
  125. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/language-tax.md +0 -0
  126. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/troubleshooting.en.md +0 -0
  127. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/troubleshooting.md +0 -0
  128. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/usage-guide.en.md +0 -0
  129. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/usage-guide.md +0 -0
  130. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/low-memory-integration.md +0 -0
  131. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/openrouter-roster/CHANGES.md +0 -0
  132. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/openrouter-roster/README.md +0 -0
  133. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/openrouter-roster/latest.json +0 -0
  134. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v0.4.md +0 -0
  135. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v0.5-verify.md +0 -0
  136. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v0.5.md +0 -0
  137. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v0.6.md +0 -0
  138. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v0.7.md +0 -0
  139. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v1.0-verify.md +0 -0
  140. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v1.0.md +0 -0
  141. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/start/quickstart.en.md +0 -0
  142. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/start/quickstart.md +0 -0
  143. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/start/when-do-i-need-coderouter.en.md +0 -0
  144. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/start/when-do-i-need-coderouter.md +0 -0
  145. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/.env.example +0 -0
  146. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/README.md +0 -0
  147. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers-multiagent.yaml +0 -0
  148. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.context-budget-test.yaml +0 -0
  149. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.llamacpp-vllm.yaml +0 -0
  150. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.nvidia-nim.yaml +0 -0
  151. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.ollama-auto-custom.yaml +0 -0
  152. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.ollama-auto.yaml +0 -0
  153. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.ollama-free-chain.yaml +0 -0
  154. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.raspberrypi.yaml +0 -0
  155. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.yaml +0 -0
  156. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/demo_traffic.sh +0 -0
  157. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/openrouter_roster_diff.py +0 -0
  158. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/smoke_v2_2.sh +0 -0
  159. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/verify-providers.yaml +0 -0
  160. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/verify_ollama_0_23.py +0 -0
  161. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/verify_v0_5.sh +0 -0
  162. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/verify_v1_0.sh +0 -0
  163. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/__init__.py +0 -0
  164. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/conftest.py +0 -0
  165. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_adapter_anthropic.py +0 -0
  166. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_audit_log.py +0 -0
  167. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_auto_router_cjk.py +0 -0
  168. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_backend_health.py +0 -0
  169. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_budget.py +0 -0
  170. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_capability.py +0 -0
  171. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_capability_registry.py +0 -0
  172. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_capability_registry_cache_control.py +0 -0
  173. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_claude_code_suitability.py +0 -0
  174. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_cli.py +0 -0
  175. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_cli_stats.py +0 -0
  176. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_config.py +0 -0
  177. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_context_budget.py +0 -0
  178. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_continuous_probe.py +0 -0
  179. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_dashboard_endpoint.py +0 -0
  180. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_doctor.py +0 -0
  181. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_doctor_apply.py +0 -0
  182. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_doctor_cache_probe.py +0 -0
  183. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_drift_actions.py +0 -0
  184. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_drift_detection.py +0 -0
  185. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_drift_detection_integration.py +0 -0
  186. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_env_file.py +0 -0
  187. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_env_security.py +0 -0
  188. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_errors.py +0 -0
  189. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_examples_yaml.py +0 -0
  190. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback.py +0 -0
  191. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback_anthropic.py +0 -0
  192. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback_cache_control.py +0 -0
  193. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback_cache_observed.py +0 -0
  194. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback_misconfig_warn.py +0 -0
  195. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback_paid_gate.py +0 -0
  196. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback_thinking.py +0 -0
  197. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_gguf_introspect.py +0 -0
  198. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_guards_tool_loop.py +0 -0
  199. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_hardware.py +0 -0
  200. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_ingress_anthropic.py +0 -0
  201. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_ingress_profile.py +0 -0
  202. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_language_tax.py +0 -0
  203. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_language_tax_integration.py +0 -0
  204. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_memory_budget.py +0 -0
  205. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_memory_pressure.py +0 -0
  206. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_cache.py +0 -0
  207. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_collector.py +0 -0
  208. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_cost.py +0 -0
  209. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_endpoint.py +0 -0
  210. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_jsonl.py +0 -0
  211. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_prometheus.py +0 -0
  212. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_prometheus_cache.py +0 -0
  213. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_openai_compat.py +0 -0
  214. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_openrouter_roster_diff.py +0 -0
  215. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_output_filters.py +0 -0
  216. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_output_filters_adapters.py +0 -0
  217. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_partial_stitch.py +0 -0
  218. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_plugins_integration.py +0 -0
  219. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_plugins_loader.py +0 -0
  220. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_plugins_registry.py +0 -0
  221. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_reasoning_strip.py +0 -0
  222. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_repair_byte_fallback.py +0 -0
  223. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_request_log.py +0 -0
  224. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_role_normalization.py +0 -0
  225. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_routing_adaptive.py +0 -0
  226. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_self_healing.py +0 -0
  227. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_setup_sh.py +0 -0
  228. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_state_store.py +0 -0
  229. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_token_estimation.py +0 -0
  230. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_token_estimation_accurate.py +0 -0
  231. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_tokens_saved_metric.py +0 -0
  232. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_tool_repair.py +0 -0
  233. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_translation_anthropic.py +0 -0
  234. {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_translation_reverse.py +0 -0
@@ -6,6 +6,194 @@ versioning follows [SemVer](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [v2.7.1] — 2026-07-04 (Native-endpoint shims + benchmarked tool-repair upgrade)
10
+
11
+ Two feature PRs driven by the 2026-07-04 competitive re-survey and the new
12
+ tool-call repair benchmark: **PR #39** (native `/v1/messages` endpoint gap
13
+ shims) and **PR #40** (lenient tool-call repair). All new behavior is
14
+ **opt-in with `off` defaults** (except the previously-404 `count_tokens`
15
+ endpoint, which is additive), no config-schema breaking changes, and the
16
+ Core runtime stays at **5 dependencies**.
17
+
18
+ ### Added
19
+
20
+ - **`POST /v1/messages/count_tokens`** (was 404). Anthropic-shape
21
+ `{"input_tokens": N}`; accurate when the provider declares
22
+ `tokenizer_path`, char/4 heuristic otherwise. Fills a gap native backends
23
+ leave open (Ollama's Anthropic-compatible API ships without
24
+ `count_tokens` / `tool_choice` / prompt caching). (PR #39)
25
+ - **`tool_choice_action: off | warn | emulate`** (profile option) +
26
+ `Capabilities.tool_choice` + registry field + `provider_supports_tool_choice()`.
27
+ `emulate` rewrites forced tool_choice (`any` / `tool`) into a system-prompt
28
+ instruction for non-supporting backends; the original request is preserved
29
+ for fallback to capable providers. New capability-degraded reason:
30
+ `unsupported-backend`. (PR #39)
31
+ - **`cache_control_action: off | strip`** (profile option). `strip` removes
32
+ `cache_control` markers before sending to non-supporting providers
33
+ (logged as `cache-control-stripped`; deliberately no tokens-saved
34
+ accounting). (PR #39)
35
+ - **Lenient tool-call repair** (`translation/tool_repair.py`): second-pass
36
+ parsing for malformed JSON (double braces, trailing commas, single quotes,
37
+ unquoted keys), key aliases (`tool`/`tool_name`, `parameters`/`input`/`args`),
38
+ and XML-flavoured forms — all gated on `allowed_tool_names`. Residue
39
+ cleanup (empty fences, `[,]`). Benchmark: recall 80.6% → **100%** on a
40
+ 55-case corpus; live fail-rate at default temperature 17% → **1%**
41
+ (qwen2.5-coder:7b × 100 requests). (PR #40)
42
+
43
+ ### Fixed
44
+
45
+ - **Two real false positives in the previous repairer** (python-fence dict
46
+ literals and documentation JSON with allowed tool names were converted
47
+ into executable calls — same class as the v2.7.0 code-eating regression).
48
+ Now fixed and gated by an expanded 12-case negative corpus. (PR #40)
49
+
50
+ ### Docs
51
+
52
+ - README repositioned for the native `/v1/messages` era: repair + guards
53
+ lead, wire translation demoted to last (passthrough note); new section
54
+ "why a router when Ollama connects directly?".
55
+
56
+ Tests: **1481 passed, 0 failed** (1401 → 1481; +37 shim, +43 lenient-repair).
57
+
58
+ ---
59
+
60
+ ## [v2.7.0] — 2026-07-02 (Reliability & security: full-source review fixes)
61
+
62
+ 22 reliability and security fixes from a full-source review (26,600 lines),
63
+ landed as two sequential PRs: high-priority **PR #34** (H1–H8) and
64
+ medium-priority **PR #35** (M1–M14), both merged to `main`. No API or
65
+ config-schema breaking changes. Backward compatibility highlights: the new
66
+ launcher token auth is **opt-in** (`CODEROUTER_LAUNCHER_TOKEN` unset ⇒
67
+ unchanged behavior + a startup warning), and the Host-validation middleware
68
+ allows loopback by default with `CODEROUTER_ALLOWED_HOSTS` to extend it.
69
+
70
+ ### Fixed
71
+
72
+ - **`/metrics` 500 when drift counters are non-zero** — three malformed
73
+ Prometheus label tuples (`(((),), v)` → `((), v)`) (`coderouter/metrics/prometheus.py`, PR #34/H1).
74
+ - **`tool_repair` deleted unrelated fenced code blocks** while stripping
75
+ tool-call-shaped ones, losing user content
76
+ (`coderouter/translation/tool_repair.py`, PR #34/H2).
77
+ - **Probe URL mismatch (`/v1/v1` duplication)** between the continuous probe
78
+ and the adapters caused healthy backends to be wrongly demoted; URL
79
+ normalization is now shared
80
+ (`coderouter/adapters/anthropic_native.py`, `coderouter/guards/continuous_probe.py`, PR #34/H4).
81
+ - **`context_budget` dropped entire history at once** on threshold breach;
82
+ trimming is now sequential per tool-pair with re-estimation after each
83
+ step, and the new head turn is renormalized to a plain user turn
84
+ (`coderouter/guards/context_budget.py`, PR #34/H5).
85
+ - **Empty-stream termination violated the SSE protocol** by sending a
86
+ terminator with no preceding `message_start`; one is now synthesized
87
+ first (`coderouter/translation/convert.py`, PR #34/H6).
88
+ - **Excluded providers never recovered after a restart** — restored
89
+ self-healing state didn't re-arm recovery probes (only the live
90
+ UNHEALTHY-transition path did); probes are now re-spawned after state
91
+ restore, queued if the event loop isn't running yet
92
+ (`coderouter/routing/fallback.py`, `coderouter/guards/self_healing.py`, PR #34/H7).
93
+ - **Malformed upstream responses bypassed retry/fallback** — an uncaught
94
+ pydantic `ValidationError` on missing/misshaped fields now converts to a
95
+ retryable `AdapterError` on non-stream paths, and stream paths skip the
96
+ malformed chunk with a once-per-stream warning
97
+ (`coderouter/adapters/anthropic_native.py`, `coderouter/adapters/openai_compat.py`, PR #35/M6).
98
+ - **`"oom"` substring matched inside `"room"` / `"zoom"` / `"bloom"`** in
99
+ upstream error bodies, cooling down healthy providers; the memory-pressure
100
+ guard now matches the token on word boundaries
101
+ (`coderouter/guards/memory_pressure.py`, PR #35/M10).
102
+ - **Interleaved text/tool-call deltas sent `input_json_delta` to
103
+ already-closed block indices** — blocks are now reopened at a fresh index
104
+ with their original `id`/`name` preserved
105
+ (`coderouter/translation/convert.py`, PR #35/M7).
106
+ - **`tool_result.is_error` was dropped in both translation directions** — an
107
+ `"Error: "` content marker now round-trips the flag without
108
+ double-prefixing (`coderouter/translation/convert.py`, PR #35/M8).
109
+ - **Streams ending without `message_stop` sent no terminal chunk**, hanging
110
+ OpenAI-compatible clients; a `finish_reason` + usage chunk is now
111
+ synthesized on abnormal termination (mirrors the H6 fix in the other
112
+ direction) (`coderouter/translation/convert.py`, PR #35/M9).
113
+ - **`configure_logging` permanently detached the metrics collector** on a
114
+ second `create_app()` call by removing every root handler;
115
+ `install_collector` now re-attaches when missing, and
116
+ `configure_logging` removes only its own marked handlers
117
+ (`coderouter/logging.py`, PR #35/M4).
118
+ - **Metrics persistence save/load asymmetry** zeroed language-tax
119
+ accounting on every restart — `save_state` was missing
120
+ `language_tax_usd` / `language_tax_usd_aggregate`, which `load_state`
121
+ expects (`coderouter/metrics/collector.py`, PR #35/M5).
122
+
123
+ ### Changed
124
+
125
+ - **Config validation now fails fast at load time.** Previously-silent
126
+ misconfiguration is now rejected: profile chains referencing unknown
127
+ provider names, duplicate provider/profile names, inverted thresholds
128
+ (e.g. `context_budget` warn > trim, `trim_target` ≥ `trim_threshold`,
129
+ `recovery_probe_initial_s` > `recovery_probe_max_s`), and
130
+ `has_tools: false` / `has_image: false` matchers (which could never
131
+ match) (`coderouter/config/schemas.py`, PR #35/M13).
132
+ - **Drift-based chain demotion now applies regardless of the `adaptive`
133
+ flag.** A dedicated cooldown-based demotion map is applied in
134
+ `_resolve_anthropic_chain` unconditionally; previously demotion only
135
+ affected ordering when `profile.adaptive` was true (and did nothing
136
+ under five samples) (`coderouter/routing/fallback.py`, PR #35/M3).
137
+ - **Adaptive routing now observes streaming paths.** `record_attempt` was
138
+ only called from `generate_anthropic`; all four entry points
139
+ (`stream_anthropic` / `stream` / `generate` and non-stream) now record
140
+ first-event latency and outcomes, including Claude Code's default
141
+ streaming path (`coderouter/routing/fallback.py`, PR #35/M2).
142
+ - **Drift verdicts are now request-scoped (`ContextVar`)** instead of
143
+ stored on the shared engine, eliminating a race where concurrent
144
+ requests could read another request's drift header or a stale verdict
145
+ during cooldown; the engine attribute remains as a deprecated mirror
146
+ (`coderouter/routing/fallback.py`, PR #35/M1).
147
+ - **Chain resolution and context estimation no longer run twice per
148
+ request** — the resolved dispatch is cached per request and reused when
149
+ the request object is unchanged (`coderouter/routing/fallback.py`, PR #35/M11).
150
+ - **Request/audit logs are now buffered** (20 records or 2 seconds,
151
+ flushed on close/`atexit`; `flush_every_n=1` restores write-through)
152
+ instead of synchronous per-request writes
153
+ (`coderouter/state/audit_log.py`, `coderouter/state/request_log.py`, PR #35/M12).
154
+ - **The metrics collector hot path exits early** for unrecognized events
155
+ before acquiring the lock, and known events are dispatched via a dict
156
+ table instead of a ~30-branch `if`/`elif` chain
157
+ (`coderouter/metrics/collector.py`, PR #35/M12).
158
+ - **Adapters now share a single `httpx.AsyncClient`** (lazily created,
159
+ per-call timeout override, closed on shutdown) instead of creating one
160
+ per request, enabling keep-alive/TLS session reuse
161
+ (`coderouter/adapters/base.py`, `coderouter/adapters/openai_compat.py`,
162
+ `coderouter/adapters/anthropic_native.py`, `coderouter/ingress/app.py`, PR #34/H3).
163
+
164
+ ### Security
165
+
166
+ - **Host-validation middleware** on all routes rejects non-loopback `Host`
167
+ headers as a DNS-rebinding defense; loopback
168
+ (`localhost` / `127.0.0.1` / `[::1]`) is allowed by default and
169
+ `CODEROUTER_ALLOWED_HOSTS` (comma-separated) extends the allowlist
170
+ (`coderouter/ingress/app.py`, PR #34/H8).
171
+ - **Opt-in launcher token authentication.** `launcher start/stop/delete`
172
+ require the `X-CodeRouter-Token` header (constant-time compare) when
173
+ `CODEROUTER_LAUNCHER_TOKEN` is set; unset behaves exactly as before, with
174
+ a one-time startup warning (`coderouter/ingress/launcher_routes.py`, PR #34/H8).
175
+ - **`-m`/`--model` re-specification rejected** in launcher `extra_args`/
176
+ `options` to block arbitrary model-path injection
177
+ (`coderouter/ingress/launcher_routes.py`, PR #34/H8).
178
+ - **Request body size cap** (64 MB default, `CODEROUTER_MAX_BODY_BYTES`
179
+ override) returns 413; SSE responses are unaffected
180
+ (`coderouter/ingress/app.py`, PR #35/M14).
181
+ - **`/api/launcher/suggest` path confinement** — resolved paths must now
182
+ fall inside the configured `model_dirs`, closing an arbitrary-path
183
+ existence/size probe (`coderouter/ingress/launcher_routes.py`, PR #35/M14).
184
+
185
+ ### Tests
186
+
187
+ - Full suite grew from **1263 → 1401 passed** (1 skipped, environment-only)
188
+ across the two PRs: 139 new regression tests in 12 new test files
189
+ (`tests/test_fix_h*.py` ×6, `tests/test_fix_m*.py` ×6). One existing test,
190
+ `tests/test_auto_router.py::test_has_tools_false_rejected_at_load`, was
191
+ flipped to expect `ValidationError` — its own docstring had anticipated
192
+ this once the new config validator (M13) landed; it is the only existing
193
+ test changed by either PR.
194
+
195
+ ---
196
+
9
197
  ## [v2.6.1] — 2026-06-28 (Token-savings accounting)
10
198
 
11
199
  Patch release: surfaces **token-savings accounting** in the metrics layer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.6.1
3
+ Version: 2.7.1
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
 
@@ -35,6 +35,7 @@ from collections.abc import AsyncIterator
35
35
  from typing import Any
36
36
 
37
37
  import httpx
38
+ from pydantic import ValidationError
38
39
 
39
40
  from coderouter.adapters.base import (
40
41
  AdapterError,
@@ -68,6 +69,26 @@ _RETRYABLE_STATUSES = {404, 408, 425, 429, 500, 502, 503, 504}
68
69
  _DEFAULT_ANTHROPIC_VERSION = "2023-06-01"
69
70
 
70
71
 
72
+ def anthropic_messages_url(base_url: str) -> str:
73
+ """Resolve the ``/v1/messages`` endpoint URL from a ``base_url``.
74
+
75
+ ``base_url`` may be given with or without a trailing ``/v1`` — users
76
+ may point it at either ``https://api.anthropic.com`` or
77
+ ``https://api.anthropic.com/v1`` (LM Studio and similar servers
78
+ commonly advertise the latter). We normalize by stripping a trailing
79
+ ``/v1`` so appending ``/v1/messages`` always yields a valid,
80
+ non-duplicated URL.
81
+
82
+ Shared by :class:`AnthropicAdapter` and the continuous-probe guard so
83
+ both hit the exact same endpoint (see bug H4: a mismatch made the
84
+ probe target ``/v1/v1/messages`` and 404 on healthy backends).
85
+ """
86
+ base = base_url.rstrip("/")
87
+ if base.endswith("/v1"):
88
+ base = base[: -len("/v1")]
89
+ return f"{base}/v1/messages"
90
+
91
+
71
92
  class AnthropicAdapter(BaseAdapter):
72
93
  """Native Anthropic Messages API adapter (passthrough).
73
94
 
@@ -87,15 +108,10 @@ class AnthropicAdapter(BaseAdapter):
87
108
 
88
109
  ``base_url`` may be given with or without a trailing ``/v1`` —
89
110
  we normalize by stripping it so appending ``/v1/messages``
90
- always yields a valid URL.
111
+ always yields a valid URL. Delegates to the shared
112
+ :func:`anthropic_messages_url` helper.
91
113
  """
92
- base = str(self.config.base_url).rstrip("/")
93
- # Users may point base_url at either `https://api.anthropic.com`
94
- # or `https://api.anthropic.com/v1`. We normalize to the former so
95
- # we can always append /v1/messages.
96
- if base.endswith("/v1"):
97
- base = base[: -len("/v1")]
98
- return f"{base}/v1/messages"
114
+ return anthropic_messages_url(str(self.config.base_url))
99
115
 
100
116
  def _headers(self, request: AnthropicRequest | None = None) -> dict[str, str]:
101
117
  """Build Anthropic-native HTTP headers, including beta-header forwarding.
@@ -161,21 +177,21 @@ class AnthropicAdapter(BaseAdapter):
161
177
  that auth works and the endpoint is reachable.
162
178
  """
163
179
  try:
164
- async with httpx.AsyncClient(timeout=5.0) as client:
165
- resp = await client.post(
166
- self._url(),
167
- headers=self._headers(),
168
- json={
169
- "model": self.config.model,
170
- "max_tokens": 1,
171
- "messages": [{"role": "user", "content": "ping"}],
172
- },
173
- )
174
- # 200 is clearly healthy. 4xx auth errors indicate the
175
- # endpoint is reachable even if the key is bad — still a
176
- # "the server answered" signal for healthcheck purposes.
177
- # 5xx is upstream trouble.
178
- return resp.status_code < 500
180
+ resp = await self.client().post(
181
+ self._url(),
182
+ headers=self._headers(),
183
+ json={
184
+ "model": self.config.model,
185
+ "max_tokens": 1,
186
+ "messages": [{"role": "user", "content": "ping"}],
187
+ },
188
+ timeout=5.0,
189
+ )
190
+ # 200 is clearly healthy. 4xx auth errors indicate the
191
+ # endpoint is reachable even if the key is bad — still a
192
+ # "the server answered" signal for healthcheck purposes.
193
+ # 5xx is upstream trouble.
194
+ return resp.status_code < 500
179
195
  except httpx.HTTPError:
180
196
  return False
181
197
 
@@ -331,8 +347,9 @@ class AnthropicAdapter(BaseAdapter):
331
347
  timeout = self.effective_timeout(overrides)
332
348
 
333
349
  try:
334
- async with httpx.AsyncClient(timeout=timeout) as client:
335
- resp = await client.post(url, json=payload, headers=self._headers(request))
350
+ resp = await self.client().post(
351
+ url, json=payload, headers=self._headers(request), timeout=timeout
352
+ )
336
353
  except httpx.TimeoutException as exc:
337
354
  raise AdapterError(
338
355
  f"timeout contacting {url}", provider=self.name, retryable=True
@@ -394,7 +411,22 @@ class AnthropicAdapter(BaseAdapter):
394
411
  # (future additions like thinking blocks) pass through via
395
412
  # extra="allow" on AnthropicResponse.
396
413
  data["coderouter_provider"] = self.name
397
- return AnthropicResponse.model_validate(data)
414
+ # M6: guard the model validation. A 200 body that isn't a valid
415
+ # Anthropic Messages response (missing ``content``/``id``, an
416
+ # error envelope returned with a 200, etc.) would otherwise raise
417
+ # a bare pydantic ValidationError that escapes the engine's
418
+ # AdapterError-based retry/fallback path. Convert to a retryable
419
+ # AdapterError so the chain can fall through (mirrors the
420
+ # invalid-JSON branch above, but retryable — a malformed shape is
421
+ # usually transient upstream noise rather than a config fault).
422
+ try:
423
+ return AnthropicResponse.model_validate(data)
424
+ except ValidationError as exc:
425
+ raise AdapterError(
426
+ f"malformed response shape from upstream: {exc}",
427
+ provider=self.name,
428
+ retryable=True,
429
+ ) from exc
398
430
 
399
431
  async def stream_anthropic(
400
432
  self,
@@ -424,10 +456,14 @@ class AnthropicAdapter(BaseAdapter):
424
456
  logged_flag: list[bool] = [False]
425
457
 
426
458
  try:
427
- async with (
428
- httpx.AsyncClient(timeout=timeout) as client,
429
- client.stream("POST", url, json=payload, headers=self._headers(request)) as resp,
430
- ):
459
+ # H3: stream over the shared client (pool / keep-alive / TLS
460
+ # reuse). Only the ``stream(...)`` context is scoped here; the
461
+ # client persists and is closed via ``aclose`` on shutdown. The
462
+ # ``async with`` releases the response's borrowed connection even
463
+ # if the caller abandons this generator mid-stream — no GC leak.
464
+ async with self.client().stream(
465
+ "POST", url, json=payload, headers=self._headers(request), timeout=timeout
466
+ ) as resp:
431
467
  if resp.status_code >= 400:
432
468
  body = await resp.aread()
433
469
  raise AdapterError(
@@ -11,6 +11,7 @@ from abc import ABC, abstractmethod
11
11
  from collections.abc import AsyncIterator
12
12
  from typing import Any, Literal
13
13
 
14
+ import httpx
14
15
  from pydantic import BaseModel, ConfigDict, Field
15
16
 
16
17
  from coderouter.config.schemas import ProviderConfig
@@ -164,17 +165,59 @@ class BaseAdapter(ABC):
164
165
  def __init__(self, config: ProviderConfig) -> None:
165
166
  """Bind the adapter to a :class:`ProviderConfig`.
166
167
 
167
- Subclasses do not need to override this; HTTP clients are
168
- constructed lazily inside :meth:`generate` / :meth:`stream` so
169
- each call can honor a per-call timeout override.
168
+ Subclasses do not need to override this. A single shared
169
+ :class:`httpx.AsyncClient` is created lazily on first use (see
170
+ :meth:`client`) and reused across every call on this adapter so
171
+ the connection pool, HTTP keep-alive, and TLS session are all
172
+ reused. Per-call timeouts are still honored by passing
173
+ ``timeout=`` to ``client.post`` / ``client.stream`` — they do
174
+ NOT require a fresh client.
170
175
  """
171
176
  self.config = config
177
+ # Shared client, created lazily inside the running event loop the
178
+ # first time an adapter method needs it (see ``client``). Kept as
179
+ # ``None`` until then so constructing an adapter has no I/O cost and
180
+ # is safe outside an event loop (e.g. at import / config time).
181
+ self._client: httpx.AsyncClient | None = None
172
182
 
173
183
  @property
174
184
  def name(self) -> str:
175
185
  """Shortcut for ``self.config.name`` — used in log trails and errors."""
176
186
  return self.config.name
177
187
 
188
+ # ---- H3: shared HTTP client (connection-pool / keep-alive reuse) ----
189
+ def client(self) -> httpx.AsyncClient:
190
+ """Return the shared :class:`httpx.AsyncClient`, creating it lazily.
191
+
192
+ The client is created on first use so construction happens inside
193
+ the running event loop and carries no I/O cost at adapter-build
194
+ time. Subsequent calls return the same instance, which is what
195
+ lets httpx reuse pooled connections, keep-alive, and the TLS
196
+ session across requests.
197
+
198
+ No default timeout is baked in here: every call site passes an
199
+ explicit per-call ``timeout=`` (resolved from the active profile
200
+ via :meth:`effective_timeout`), so leaving the client timeout
201
+ unset avoids a surprising default clamping long-running calls.
202
+ """
203
+ if self._client is None:
204
+ self._client = httpx.AsyncClient(timeout=None)
205
+ return self._client
206
+
207
+ async def aclose(self) -> None:
208
+ """Close the shared HTTP client and drop the reference.
209
+
210
+ Idempotent: safe to call when no client was ever created, and
211
+ safe to call more than once. Invoked from the app lifespan
212
+ shutdown path so pooled connections are released cleanly rather
213
+ than left to garbage collection. After ``aclose`` a later call
214
+ re-creates the client on demand via :meth:`client`.
215
+ """
216
+ client = self._client
217
+ self._client = None
218
+ if client is not None:
219
+ await client.aclose()
220
+
178
221
  # ---- v0.6-B override resolution helpers -----------------------------
179
222
  def effective_timeout(self, overrides: ProviderCallOverrides | None) -> float:
180
223
  """Profile override wins when set; else provider default."""