coderouter-cli 2.6.0__tar.gz → 2.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/.gitignore +1 -0
  2. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/CHANGELOG.md +182 -0
  3. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/PKG-INFO +32 -1
  4. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/README.en.md +31 -0
  5. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/README.md +31 -0
  6. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/adapters/anthropic_native.py +66 -30
  7. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/adapters/base.py +46 -3
  8. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/adapters/openai_compat.py +53 -11
  9. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/config/schemas.py +154 -0
  10. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/context_budget.py +114 -19
  11. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/continuous_probe.py +9 -1
  12. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/memory_pressure.py +16 -2
  13. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/self_healing.py +14 -0
  14. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/anthropic_routes.py +115 -1
  15. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/app.py +162 -1
  16. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/dashboard_routes.py +23 -0
  17. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/launcher_routes.py +187 -11
  18. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/openai_routes.py +77 -7
  19. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/logging.py +24 -3
  20. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/metrics/collector.py +479 -210
  21. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/metrics/prometheus.py +3 -3
  22. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/routing/fallback.py +387 -27
  23. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/state/audit_log.py +69 -7
  24. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/state/request_log.py +70 -7
  25. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/translation/convert.py +112 -9
  26. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/translation/tool_repair.py +39 -23
  27. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/README.md +38 -1
  28. coderouter_cli-2.7.0/docs/backends/claude-code-llamacpp-vllm.md +172 -0
  29. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/launcher.md +2 -0
  30. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/concepts/architecture.md +1 -1
  31. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/concepts/drift-detection.md +3 -2
  32. coderouter_cli-2.7.0/docs/guides/language-tax.en.md +138 -0
  33. coderouter_cli-2.7.0/docs/guides/language-tax.md +139 -0
  34. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/security.en.md +17 -3
  35. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/security.md +3 -2
  36. coderouter_cli-2.7.0/examples/README.md +68 -0
  37. coderouter_cli-2.7.0/examples/providers-multiagent.yaml +188 -0
  38. coderouter_cli-2.6.0/examples/providers.v2-context-budget.yaml → coderouter_cli-2.7.0/examples/providers.context-budget-test.yaml +2 -1
  39. coderouter_cli-2.6.0/examples/providers.llama-cpp-vllm.yaml → coderouter_cli-2.7.0/examples/providers.llamacpp-vllm.yaml +19 -9
  40. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/examples/providers.nvidia-nim.yaml +1 -0
  41. coderouter_cli-2.6.0/examples/providers.auto-custom.yaml → coderouter_cli-2.7.0/examples/providers.ollama-auto-custom.yaml +6 -5
  42. coderouter_cli-2.6.0/examples/providers.auto.yaml → coderouter_cli-2.7.0/examples/providers.ollama-auto.yaml +2 -1
  43. coderouter_cli-2.6.0/examples/providers.note-2026.yaml → coderouter_cli-2.7.0/examples/providers.ollama-free-chain.yaml +3 -2
  44. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/examples/providers.raspberrypi.yaml +1 -0
  45. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/examples/providers.yaml +1 -0
  46. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/pyproject.toml +1 -1
  47. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_auto_router.py +9 -11
  48. coderouter_cli-2.7.0/tests/test_fix_h1_prometheus_drift.py +102 -0
  49. coderouter_cli-2.7.0/tests/test_fix_h2_h6_translation.py +114 -0
  50. coderouter_cli-2.7.0/tests/test_fix_h3_client_reuse.py +188 -0
  51. coderouter_cli-2.7.0/tests/test_fix_h4_h5_guards.py +289 -0
  52. coderouter_cli-2.7.0/tests/test_fix_h7_probe_restore.py +152 -0
  53. coderouter_cli-2.7.0/tests/test_fix_h8_launcher_auth.py +316 -0
  54. coderouter_cli-2.7.0/tests/test_fix_mA_routing.py +616 -0
  55. coderouter_cli-2.7.0/tests/test_fix_mB_adapters.py +215 -0
  56. coderouter_cli-2.7.0/tests/test_fix_mC_convert.py +494 -0
  57. coderouter_cli-2.7.0/tests/test_fix_mD_logging_metrics.py +410 -0
  58. coderouter_cli-2.7.0/tests/test_fix_mE_config_validation.py +260 -0
  59. coderouter_cli-2.7.0/tests/test_fix_mF_ingress.py +349 -0
  60. coderouter_cli-2.7.0/tests/test_tokens_saved_metric.py +87 -0
  61. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/LICENSE +0 -0
  62. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/__init__.py +0 -0
  63. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/__main__.py +0 -0
  64. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/adapters/__init__.py +0 -0
  65. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/adapters/registry.py +0 -0
  66. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/cli.py +0 -0
  67. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/cli_stats.py +0 -0
  68. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/config/__init__.py +0 -0
  69. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/config/capability_registry.py +0 -0
  70. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/config/env_file.py +0 -0
  71. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/config/loader.py +0 -0
  72. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/cost.py +0 -0
  73. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/data/__init__.py +0 -0
  74. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/data/model-capabilities.yaml +0 -0
  75. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/doctor.py +0 -0
  76. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/doctor_apply.py +0 -0
  77. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/env_security.py +0 -0
  78. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/errors.py +0 -0
  79. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/gguf_introspect.py +0 -0
  80. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/__init__.py +0 -0
  81. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/_fingerprint.py +0 -0
  82. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/backend_health.py +0 -0
  83. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/drift_actions.py +0 -0
  84. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/drift_detection.py +0 -0
  85. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/memory_budget.py +0 -0
  86. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/tool_loop.py +0 -0
  87. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/hardware.py +0 -0
  88. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/__init__.py +0 -0
  89. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/metrics_routes.py +0 -0
  90. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/language_tax.py +0 -0
  91. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/metrics/__init__.py +0 -0
  92. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/output_filters.py +0 -0
  93. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/plugins/__init__.py +0 -0
  94. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/plugins/base.py +0 -0
  95. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/plugins/loader.py +0 -0
  96. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/plugins/registry.py +0 -0
  97. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/routing/__init__.py +0 -0
  98. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/routing/adaptive.py +0 -0
  99. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/routing/auto_router.py +0 -0
  100. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/routing/budget.py +0 -0
  101. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/routing/capability.py +0 -0
  102. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/state/__init__.py +0 -0
  103. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/state/replay.py +0 -0
  104. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/state/store.py +0 -0
  105. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/state/suggest_rules.py +0 -0
  106. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/token_estimation.py +0 -0
  107. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/token_estimation_accurate.py +0 -0
  108. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/translation/__init__.py +0 -0
  109. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/translation/anthropic.py +0 -0
  110. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/assets/dashboard-demo.png +0 -0
  111. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/gguf_dl.md +0 -0
  112. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/hf-ollama-models.md +0 -0
  113. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/install-backends.en.md +0 -0
  114. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/install-backends.md +0 -0
  115. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/launcher-quickstart.md +0 -0
  116. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/llamacpp-direct.en.md +0 -0
  117. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/llamacpp-direct.md +0 -0
  118. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/lmstudio-direct.en.md +0 -0
  119. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/lmstudio-direct.md +0 -0
  120. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/verify-ollama-0.23.1.md +0 -0
  121. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/concepts/context-budget.md +0 -0
  122. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/concepts/continuous-probing.md +0 -0
  123. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/concepts/partial-stitch.md +0 -0
  124. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  125. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/designs/v1.6-auto-router-verification.md +0 -0
  126. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/designs/v1.6-auto-router.md +0 -0
  127. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/free-tier-guide.en.md +0 -0
  128. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/free-tier-guide.md +0 -0
  129. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/troubleshooting.en.md +0 -0
  130. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/troubleshooting.md +0 -0
  131. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/usage-guide.en.md +0 -0
  132. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/usage-guide.md +0 -0
  133. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/low-memory-integration.md +0 -0
  134. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/openrouter-roster/CHANGES.md +0 -0
  135. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/openrouter-roster/README.md +0 -0
  136. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/openrouter-roster/latest.json +0 -0
  137. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v0.4.md +0 -0
  138. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v0.5-verify.md +0 -0
  139. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v0.5.md +0 -0
  140. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v0.6.md +0 -0
  141. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v0.7.md +0 -0
  142. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v1.0-verify.md +0 -0
  143. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v1.0.md +0 -0
  144. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/start/quickstart.en.md +0 -0
  145. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/start/quickstart.md +0 -0
  146. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/start/when-do-i-need-coderouter.en.md +0 -0
  147. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/start/when-do-i-need-coderouter.md +0 -0
  148. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/examples/.env.example +0 -0
  149. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/demo_traffic.sh +0 -0
  150. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/openrouter_roster_diff.py +0 -0
  151. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/smoke_v2_2.sh +0 -0
  152. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/verify-providers.yaml +0 -0
  153. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/verify_ollama_0_23.py +0 -0
  154. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/verify_v0_5.sh +0 -0
  155. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/verify_v1_0.sh +0 -0
  156. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/__init__.py +0 -0
  157. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/conftest.py +0 -0
  158. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_adapter_anthropic.py +0 -0
  159. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_audit_log.py +0 -0
  160. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_auto_router_cjk.py +0 -0
  161. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_backend_health.py +0 -0
  162. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_budget.py +0 -0
  163. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_capability.py +0 -0
  164. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_capability_degraded_payload.py +0 -0
  165. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_capability_registry.py +0 -0
  166. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_capability_registry_cache_control.py +0 -0
  167. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_claude_code_suitability.py +0 -0
  168. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_cli.py +0 -0
  169. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_cli_stats.py +0 -0
  170. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_config.py +0 -0
  171. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_context_budget.py +0 -0
  172. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_continuous_probe.py +0 -0
  173. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_dashboard_endpoint.py +0 -0
  174. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_doctor.py +0 -0
  175. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_doctor_apply.py +0 -0
  176. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_doctor_cache_probe.py +0 -0
  177. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_drift_actions.py +0 -0
  178. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_drift_detection.py +0 -0
  179. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_drift_detection_integration.py +0 -0
  180. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_env_file.py +0 -0
  181. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_env_security.py +0 -0
  182. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_errors.py +0 -0
  183. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_examples_yaml.py +0 -0
  184. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback.py +0 -0
  185. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback_anthropic.py +0 -0
  186. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback_cache_control.py +0 -0
  187. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback_cache_observed.py +0 -0
  188. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback_misconfig_warn.py +0 -0
  189. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback_paid_gate.py +0 -0
  190. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback_thinking.py +0 -0
  191. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_gguf_introspect.py +0 -0
  192. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_guards_tool_loop.py +0 -0
  193. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_hardware.py +0 -0
  194. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_ingress_anthropic.py +0 -0
  195. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_ingress_profile.py +0 -0
  196. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_language_tax.py +0 -0
  197. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_language_tax_integration.py +0 -0
  198. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_memory_budget.py +0 -0
  199. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_memory_pressure.py +0 -0
  200. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_cache.py +0 -0
  201. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_collector.py +0 -0
  202. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_cost.py +0 -0
  203. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_endpoint.py +0 -0
  204. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_jsonl.py +0 -0
  205. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_prometheus.py +0 -0
  206. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_prometheus_cache.py +0 -0
  207. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_openai_compat.py +0 -0
  208. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_openrouter_roster_diff.py +0 -0
  209. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_output_filters.py +0 -0
  210. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_output_filters_adapters.py +0 -0
  211. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_partial_stitch.py +0 -0
  212. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_plugins_integration.py +0 -0
  213. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_plugins_loader.py +0 -0
  214. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_plugins_registry.py +0 -0
  215. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_reasoning_strip.py +0 -0
  216. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_repair_byte_fallback.py +0 -0
  217. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_request_log.py +0 -0
  218. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_role_normalization.py +0 -0
  219. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_routing_adaptive.py +0 -0
  220. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_self_healing.py +0 -0
  221. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_setup_sh.py +0 -0
  222. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_state_store.py +0 -0
  223. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_token_estimation.py +0 -0
  224. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_token_estimation_accurate.py +0 -0
  225. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_tool_repair.py +0 -0
  226. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_translation_anthropic.py +0 -0
  227. {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_translation_reverse.py +0 -0
@@ -36,6 +36,7 @@ ENV/
36
36
  # _OUTPUTS
37
37
  # ============================================================
38
38
  _OUTPUTS/
39
+ _run/
39
40
 
40
41
  # uv
41
42
  # Note: uv.lock SHOULD be committed (per plan.md §5.4)
@@ -6,6 +6,188 @@ versioning follows [SemVer](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [v2.7.0] — 2026-07-02 (Reliability & security: full-source review fixes)
10
+
11
+ 22 reliability and security fixes from a full-source review (26,600 lines),
12
+ landed as two sequential PRs: high-priority **PR #34** (H1–H8) and
13
+ medium-priority **PR #35** (M1–M14), both merged to `main`. No API or
14
+ config-schema breaking changes. Backward compatibility highlights: the new
15
+ launcher token auth is **opt-in** (`CODEROUTER_LAUNCHER_TOKEN` unset ⇒
16
+ unchanged behavior + a startup warning), and the Host-validation middleware
17
+ allows loopback by default with `CODEROUTER_ALLOWED_HOSTS` to extend it.
18
+
19
+ ### Fixed
20
+
21
+ - **`/metrics` 500 when drift counters are non-zero** — three malformed
22
+ Prometheus label tuples (`(((),), v)` → `((), v)`) (`coderouter/metrics/prometheus.py`, PR #34/H1).
23
+ - **`tool_repair` deleted unrelated fenced code blocks** while stripping
24
+ tool-call-shaped ones, losing user content
25
+ (`coderouter/translation/tool_repair.py`, PR #34/H2).
26
+ - **Probe URL mismatch (`/v1/v1` duplication)** between the continuous probe
27
+ and the adapters caused healthy backends to be wrongly demoted; URL
28
+ normalization is now shared
29
+ (`coderouter/adapters/anthropic_native.py`, `coderouter/guards/continuous_probe.py`, PR #34/H4).
30
+ - **`context_budget` dropped entire history at once** on threshold breach;
31
+ trimming is now sequential per tool-pair with re-estimation after each
32
+ step, and the new head turn is renormalized to a plain user turn
33
+ (`coderouter/guards/context_budget.py`, PR #34/H5).
34
+ - **Empty-stream termination violated the SSE protocol** by sending a
35
+ terminator with no preceding `message_start`; one is now synthesized
36
+ first (`coderouter/translation/convert.py`, PR #34/H6).
37
+ - **Excluded providers never recovered after a restart** — restored
38
+ self-healing state didn't re-arm recovery probes (only the live
39
+ UNHEALTHY-transition path did); probes are now re-spawned after state
40
+ restore, queued if the event loop isn't running yet
41
+ (`coderouter/routing/fallback.py`, `coderouter/guards/self_healing.py`, PR #34/H7).
42
+ - **Malformed upstream responses bypassed retry/fallback** — an uncaught
43
+ pydantic `ValidationError` on missing/misshaped fields now converts to a
44
+ retryable `AdapterError` on non-stream paths, and stream paths skip the
45
+ malformed chunk with a once-per-stream warning
46
+ (`coderouter/adapters/anthropic_native.py`, `coderouter/adapters/openai_compat.py`, PR #35/M6).
47
+ - **`"oom"` substring matched inside `"room"` / `"zoom"` / `"bloom"`** in
48
+ upstream error bodies, cooling down healthy providers; the memory-pressure
49
+ guard now matches the token on word boundaries
50
+ (`coderouter/guards/memory_pressure.py`, PR #35/M10).
51
+ - **Interleaved text/tool-call deltas sent `input_json_delta` to
52
+ already-closed block indices** — blocks are now reopened at a fresh index
53
+ with their original `id`/`name` preserved
54
+ (`coderouter/translation/convert.py`, PR #35/M7).
55
+ - **`tool_result.is_error` was dropped in both translation directions** — an
56
+ `"Error: "` content marker now round-trips the flag without
57
+ double-prefixing (`coderouter/translation/convert.py`, PR #35/M8).
58
+ - **Streams ending without `message_stop` sent no terminal chunk**, hanging
59
+ OpenAI-compatible clients; a `finish_reason` + usage chunk is now
60
+ synthesized on abnormal termination (mirrors the H6 fix in the other
61
+ direction) (`coderouter/translation/convert.py`, PR #35/M9).
62
+ - **`configure_logging` permanently detached the metrics collector** on a
63
+ second `create_app()` call by removing every root handler;
64
+ `install_collector` now re-attaches when missing, and
65
+ `configure_logging` removes only its own marked handlers
66
+ (`coderouter/logging.py`, PR #35/M4).
67
+ - **Metrics persistence save/load asymmetry** zeroed language-tax
68
+ accounting on every restart — `save_state` was missing
69
+ `language_tax_usd` / `language_tax_usd_aggregate`, which `load_state`
70
+ expects (`coderouter/metrics/collector.py`, PR #35/M5).
71
+
72
+ ### Changed
73
+
74
+ - **Config validation now fails fast at load time.** Previously-silent
75
+ misconfiguration is now rejected: profile chains referencing unknown
76
+ provider names, duplicate provider/profile names, inverted thresholds
77
+ (e.g. `context_budget` warn > trim, `trim_target` ≥ `trim_threshold`,
78
+ `recovery_probe_initial_s` > `recovery_probe_max_s`), and
79
+ `has_tools: false` / `has_image: false` matchers (which could never
80
+ match) (`coderouter/config/schemas.py`, PR #35/M13).
81
+ - **Drift-based chain demotion now applies regardless of the `adaptive`
82
+ flag.** A dedicated cooldown-based demotion map is applied in
83
+ `_resolve_anthropic_chain` unconditionally; previously demotion only
84
+ affected ordering when `profile.adaptive` was true (and did nothing
85
+ under five samples) (`coderouter/routing/fallback.py`, PR #35/M3).
86
+ - **Adaptive routing now observes streaming paths.** `record_attempt` was
87
+ only called from `generate_anthropic`; all four entry points
88
+ (`stream_anthropic` / `stream` / `generate` and non-stream) now record
89
+ first-event latency and outcomes, including Claude Code's default
90
+ streaming path (`coderouter/routing/fallback.py`, PR #35/M2).
91
+ - **Drift verdicts are now request-scoped (`ContextVar`)** instead of
92
+ stored on the shared engine, eliminating a race where concurrent
93
+ requests could read another request's drift header or a stale verdict
94
+ during cooldown; the engine attribute remains as a deprecated mirror
95
+ (`coderouter/routing/fallback.py`, PR #35/M1).
96
+ - **Chain resolution and context estimation no longer run twice per
97
+ request** — the resolved dispatch is cached per request and reused when
98
+ the request object is unchanged (`coderouter/routing/fallback.py`, PR #35/M11).
99
+ - **Request/audit logs are now buffered** (20 records or 2 seconds,
100
+ flushed on close/`atexit`; `flush_every_n=1` restores write-through)
101
+ instead of synchronous per-request writes
102
+ (`coderouter/state/audit_log.py`, `coderouter/state/request_log.py`, PR #35/M12).
103
+ - **The metrics collector hot path exits early** for unrecognized events
104
+ before acquiring the lock, and known events are dispatched via a dict
105
+ table instead of a ~30-branch `if`/`elif` chain
106
+ (`coderouter/metrics/collector.py`, PR #35/M12).
107
+ - **Adapters now share a single `httpx.AsyncClient`** (lazily created,
108
+ per-call timeout override, closed on shutdown) instead of creating one
109
+ per request, enabling keep-alive/TLS session reuse
110
+ (`coderouter/adapters/base.py`, `coderouter/adapters/openai_compat.py`,
111
+ `coderouter/adapters/anthropic_native.py`, `coderouter/ingress/app.py`, PR #34/H3).
112
+
113
+ ### Security
114
+
115
+ - **Host-validation middleware** on all routes rejects non-loopback `Host`
116
+ headers as a DNS-rebinding defense; loopback
117
+ (`localhost` / `127.0.0.1` / `[::1]`) is allowed by default and
118
+ `CODEROUTER_ALLOWED_HOSTS` (comma-separated) extends the allowlist
119
+ (`coderouter/ingress/app.py`, PR #34/H8).
120
+ - **Opt-in launcher token authentication.** `launcher start/stop/delete`
121
+ require the `X-CodeRouter-Token` header (constant-time compare) when
122
+ `CODEROUTER_LAUNCHER_TOKEN` is set; unset behaves exactly as before, with
123
+ a one-time startup warning (`coderouter/ingress/launcher_routes.py`, PR #34/H8).
124
+ - **`-m`/`--model` re-specification rejected** in launcher `extra_args`/
125
+ `options` to block arbitrary model-path injection
126
+ (`coderouter/ingress/launcher_routes.py`, PR #34/H8).
127
+ - **Request body size cap** (64 MB default, `CODEROUTER_MAX_BODY_BYTES`
128
+ override) returns 413; SSE responses are unaffected
129
+ (`coderouter/ingress/app.py`, PR #35/M14).
130
+ - **`/api/launcher/suggest` path confinement** — resolved paths must now
131
+ fall inside the configured `model_dirs`, closing an arbitrary-path
132
+ existence/size probe (`coderouter/ingress/launcher_routes.py`, PR #35/M14).
133
+
134
+ ### Tests
135
+
136
+ - Full suite grew from **1263 → 1401 passed** (1 skipped, environment-only)
137
+ across the two PRs: 139 new regression tests in 12 new test files
138
+ (`tests/test_fix_h*.py` ×6, `tests/test_fix_m*.py` ×6). One existing test,
139
+ `tests/test_auto_router.py::test_has_tools_false_rejected_at_load`, was
140
+ flipped to expect `ValidationError` — its own docstring had anticipated
141
+ this once the new config validator (M13) landed; it is the only existing
142
+ test changed by either PR.
143
+
144
+ ---
145
+
146
+ ## [v2.6.1] — 2026-06-28 (Token-savings accounting)
147
+
148
+ Patch release: surfaces **token-savings accounting** in the metrics layer
149
+ and dashboard. The figure is owned by core, so it appears even when no
150
+ plugin is installed (trim savings from the context-budget guard), and the
151
+ optional `compress` plugin adds to the same total via a neutral
152
+ `tokens-saved` log event. **No new core dependency**, **no behavioral
153
+ change** to existing paths, and **fully backward compatible** — existing
154
+ counters and events are untouched; the new buckets are additive.
155
+
156
+ ### Added
157
+
158
+ - **Token-savings buckets in `MetricsCollector`**
159
+ (`coderouter/metrics/collector.py`). `tokens_saved_total` plus a
160
+ per-mechanism breakdown (`tokens_saved_by_mechanism`). Two feeds
161
+ aggregate under one schema: `trim` (derived from the existing
162
+ `context-budget-trimmed` event's before/after token estimate) and
163
+ `compress` (the neutral `tokens-saved` event emitted by the compress
164
+ plugin — no core import). Wired through `snapshot()`, `save_state()`,
165
+ `load_state()`, and `reset()`.
166
+ - **Dashboard Token Savings tiles**
167
+ (`coderouter/ingress/dashboard_routes.py`). Three tiles — trim /
168
+ compress / total — in the "Cost & Language Tax" panel, zero-filled so a
169
+ fresh or local-only deployment renders cleanly.
170
+
171
+ ### Docs / examples
172
+
173
+ - Reorganized provider samples under `examples/` with a category index.
174
+ - Added the language-tax guide (JA/EN) and a Claude Code +
175
+ llama.cpp/vllm backend guide.
176
+
177
+ ### Tests
178
+
179
+ - `tests/test_tokens_saved_metric.py` — trim/compress accounting,
180
+ negative-delta clamping, combined aggregate, persistence round-trip,
181
+ and reset. Full suite: **1263 passed, 1 skipped**.
182
+
183
+ ### Companion
184
+
185
+ - `coderouter-plugin-compress` emits the `tokens-saved` event (plugin
186
+ branch `feat/tokens-saved-emit`, v0.2.0). CodeRouter core works
187
+ standalone — trim savings show without the plugin.
188
+
189
+ ---
190
+
9
191
  ## [v2.6.0] — 2026-06-20 (Language Tax: measure, route, visualize)
10
192
 
11
193
  Minor release: makes the CJK **"language tax"** — cloud tokenizers bill
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.6.0
3
+ Version: 2.7.0
4
4
  Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
5
5
  Project-URL: Homepage, https://github.com/zephel01/CodeRouter
6
6
  Project-URL: Repository, https://github.com/zephel01/CodeRouter
@@ -161,6 +161,36 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
161
161
  | **`coderouter replay`** | provider 切替の効果を統計比較 (A/B 分析) / `--suggest-rules` でルール最適化提案 |
162
162
  | **Continuous Probe** | idle 時も定期的に backend を監視 |
163
163
 
164
+ ### 言語税トラッキング — v2.6.0
165
+
166
+ 日本語などの CJK テキストは、クラウドのトークナイザだと「同じ意味の英語」より多くのトークンを消費します(**実測: GPT-4o 系 o200k で平均 1.6 倍、GPT-4 系 cl100k で平均 2.0 倍**)。ローカル LLM は課金されないので、この「言語税」はクラウド利用時だけ効いてきます。CodeRouter v2.6.0 はこれを **計測・ルーティング回避・可視化** します。
167
+
168
+ | 機能 | 何をしてくれるか |
169
+ |---|---|
170
+ | **言語税の計測** | プロバイダに `tokenizer_path`(ローカルの `tokenizer.json`)を指定すると、char/4 ヒューリスティック比の実トークン倍率と割増 USD を算出(ネットワーク不要・未設定なら無効) |
171
+ | **`cjk_ratio_min` ルーティング** | CJK 比率が高いリクエストを自動でローカル LLM(課金ゼロ)へ。コードや英語はクラウドへ |
172
+ | **ダッシュボード可視化** | `/dashboard` の「Cost & Language Tax」パネルで総支出・キャッシュ節約・言語税をリアルタイム表示 |
173
+
174
+ ```yaml
175
+ # providers.yaml — CJK 多めのターンはローカルへ自動回避
176
+ auto_router:
177
+ rules:
178
+ - match: { cjk_ratio_min: 0.3 } # 日本語が3割以上 → ローカル
179
+ profile: local
180
+ - match: { has_tools: true } # ツール使用 → クラウド
181
+ profile: cloud
182
+ default_rule_profile: cloud
183
+
184
+ providers:
185
+ - name: cloud-sonnet
186
+ kind: anthropic
187
+ base_url: https://api.anthropic.com
188
+ model: claude-sonnet-4-6
189
+ tokenizer_path: ~/.coderouter/tokenizers/sonnet.json # 言語税の正確計測(任意)
190
+ ```
191
+
192
+ 詳細 → [言語税ガイド](./docs/guides/language-tax.md)
193
+
164
194
  ### Launcher — llama.cpp / vllm 起動 UI
165
195
 
166
196
  `http://localhost:8088/launcher` で開けるブラウザ UI。llama.cpp や vllm を GUI で起動・管理できます。
@@ -229,6 +259,7 @@ providers:
229
259
  | 使いこなす | [利用ガイド](./docs/guides/usage-guide.md) |
230
260
  | 無料で回す | [無料枠ガイド](./docs/guides/free-tier-guide.md) |
231
261
  | llama.cpp / vllm を GUI で起動 | [Launcher ガイド](./docs/backends/launcher.md) |
262
+ | 言語税を計測・回避する | [言語税ガイド](./docs/guides/language-tax.md) |
232
263
  | 詰まった | [トラブルシューティング](./docs/guides/troubleshooting.md) |
233
264
  | 設計を知りたい | [アーキテクチャ詳細](./docs/concepts/architecture.md) |
234
265
  | 全リリース履歴 | [CHANGELOG](./CHANGELOG.md) |
@@ -116,6 +116,36 @@ Full decision matrix → [Do I need CodeRouter?](./docs/start/when-do-i-need-cod
116
116
  | **`coderouter replay`** | Compare providers statistically (A/B analysis) / `--suggest-rules` for automated rule suggestions |
117
117
  | **Continuous Probe** | Background health monitoring even during idle |
118
118
 
119
+ ### Language Tax tracking — v2.6.0
120
+
121
+ CJK text (Japanese / Chinese / Korean) costs more tokens on cloud tokenizers than the same meaning in English (**measured: ~1.6× on average with GPT-4o-era o200k, ~2.0× with GPT-4-era cl100k**). Local models bill nothing per token, so this "language tax" only bites on the cloud leg. CodeRouter v2.6.0 makes it **measurable, routable, and visible**.
122
+
123
+ | Feature | What it does |
124
+ |---|---|
125
+ | **Language-tax measurement** | Set a provider's `tokenizer_path` (a local `tokenizer.json`) to compute the real token multiplier vs the char/4 heuristic and the extra USD. No network; inert when unset. |
126
+ | **`cjk_ratio_min` routing** | Auto-route CJK-heavy turns to a local (tax-free) model; code/English falls through to the cloud chain. |
127
+ | **Dashboard panel** | The `/dashboard` "Cost & Language Tax" panel shows total spend, cache savings, and language-tax spend live. |
128
+
129
+ ```yaml
130
+ # providers.yaml — steer CJK-heavy turns to local
131
+ auto_router:
132
+ rules:
133
+ - match: { cjk_ratio_min: 0.3 } # >=30% CJK chars -> local
134
+ profile: local
135
+ - match: { has_tools: true } # tool use -> cloud
136
+ profile: cloud
137
+ default_rule_profile: cloud
138
+
139
+ providers:
140
+ - name: cloud-sonnet
141
+ kind: anthropic
142
+ base_url: https://api.anthropic.com
143
+ model: claude-sonnet-4-6
144
+ tokenizer_path: ~/.coderouter/tokenizers/sonnet.json # accurate language-tax (optional)
145
+ ```
146
+
147
+ Details → [Language Tax guide](./docs/guides/language-tax.en.md)
148
+
119
149
  ### Launcher — llama.cpp / vllm GUI
120
150
 
121
151
  Browser UI at `http://localhost:8088/launcher` for starting and managing local inference backends.
@@ -184,6 +214,7 @@ More detail → [Usage guide](./docs/guides/usage-guide.en.md) · [Architecture]
184
214
  | Use it well | [Usage guide](./docs/guides/usage-guide.en.md) |
185
215
  | Run for free | [Free-tier guide](./docs/guides/free-tier-guide.en.md) |
186
216
  | Launch llama.cpp / vllm via GUI | [Launcher guide](./docs/backends/launcher.md) |
217
+ | Measure & avoid the language tax | [Language Tax guide](./docs/guides/language-tax.en.md) |
187
218
  | Stuck? | [Troubleshooting](./docs/guides/troubleshooting.en.md) |
188
219
  | Understand the design | [Architecture](./docs/concepts/architecture.md) |
189
220
  | Full release history | [CHANGELOG](./CHANGELOG.md) |
@@ -116,6 +116,36 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
116
116
  | **`coderouter replay`** | provider 切替の効果を統計比較 (A/B 分析) / `--suggest-rules` でルール最適化提案 |
117
117
  | **Continuous Probe** | idle 時も定期的に backend を監視 |
118
118
 
119
+ ### 言語税トラッキング — v2.6.0
120
+
121
+ 日本語などの CJK テキストは、クラウドのトークナイザだと「同じ意味の英語」より多くのトークンを消費します(**実測: GPT-4o 系 o200k で平均 1.6 倍、GPT-4 系 cl100k で平均 2.0 倍**)。ローカル LLM は課金されないので、この「言語税」はクラウド利用時だけ効いてきます。CodeRouter v2.6.0 はこれを **計測・ルーティング回避・可視化** します。
122
+
123
+ | 機能 | 何をしてくれるか |
124
+ |---|---|
125
+ | **言語税の計測** | プロバイダに `tokenizer_path`(ローカルの `tokenizer.json`)を指定すると、char/4 ヒューリスティック比の実トークン倍率と割増 USD を算出(ネットワーク不要・未設定なら無効) |
126
+ | **`cjk_ratio_min` ルーティング** | CJK 比率が高いリクエストを自動でローカル LLM(課金ゼロ)へ。コードや英語はクラウドへ |
127
+ | **ダッシュボード可視化** | `/dashboard` の「Cost & Language Tax」パネルで総支出・キャッシュ節約・言語税をリアルタイム表示 |
128
+
129
+ ```yaml
130
+ # providers.yaml — CJK 多めのターンはローカルへ自動回避
131
+ auto_router:
132
+ rules:
133
+ - match: { cjk_ratio_min: 0.3 } # 日本語が3割以上 → ローカル
134
+ profile: local
135
+ - match: { has_tools: true } # ツール使用 → クラウド
136
+ profile: cloud
137
+ default_rule_profile: cloud
138
+
139
+ providers:
140
+ - name: cloud-sonnet
141
+ kind: anthropic
142
+ base_url: https://api.anthropic.com
143
+ model: claude-sonnet-4-6
144
+ tokenizer_path: ~/.coderouter/tokenizers/sonnet.json # 言語税の正確計測(任意)
145
+ ```
146
+
147
+ 詳細 → [言語税ガイド](./docs/guides/language-tax.md)
148
+
119
149
  ### Launcher — llama.cpp / vllm 起動 UI
120
150
 
121
151
  `http://localhost:8088/launcher` で開けるブラウザ UI。llama.cpp や vllm を GUI で起動・管理できます。
@@ -184,6 +214,7 @@ providers:
184
214
  | 使いこなす | [利用ガイド](./docs/guides/usage-guide.md) |
185
215
  | 無料で回す | [無料枠ガイド](./docs/guides/free-tier-guide.md) |
186
216
  | llama.cpp / vllm を GUI で起動 | [Launcher ガイド](./docs/backends/launcher.md) |
217
+ | 言語税を計測・回避する | [言語税ガイド](./docs/guides/language-tax.md) |
187
218
  | 詰まった | [トラブルシューティング](./docs/guides/troubleshooting.md) |
188
219
  | 設計を知りたい | [アーキテクチャ詳細](./docs/concepts/architecture.md) |
189
220
  | 全リリース履歴 | [CHANGELOG](./CHANGELOG.md) |
@@ -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."""