coderouter-cli 2.14.0__tar.gz → 2.15.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 (317) hide show
  1. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/CHANGELOG.md +178 -0
  2. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/PKG-INFO +17 -5
  3. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/README.en.md +15 -3
  4. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/README.md +15 -3
  5. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/adapters/anthropic_native.py +106 -1
  6. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/adapters/base.py +65 -0
  7. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/adapters/openai_compat.py +60 -0
  8. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/config/schemas.py +65 -0
  9. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/doctor.py +48 -1
  10. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/ingress/anthropic_routes.py +82 -12
  11. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/ingress/openai_routes.py +31 -2
  12. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/logging.py +143 -0
  13. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/metrics/collector.py +40 -0
  14. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/metrics/prometheus.py +46 -0
  15. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/routing/__init__.py +8 -0
  16. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/routing/fallback.py +304 -4
  17. coderouter_cli-2.15.1/coderouter/routing/fallback_trace.py +501 -0
  18. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/state/audit_log.py +4 -0
  19. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/vscode_init.py +10 -3
  20. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/README.en.md +30 -8
  21. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/README.md +35 -13
  22. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/claude-code-llamacpp-vllm.en.md +2 -0
  23. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/claude-code-llamacpp-vllm.md +2 -0
  24. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/install-backends.en.md +2 -0
  25. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/install-backends.md +2 -0
  26. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/launcher.en.md +3 -1
  27. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/launcher.md +3 -1
  28. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/llamacpp-direct.en.md +3 -0
  29. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/llamacpp-direct.md +2 -0
  30. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/concepts/architecture.en.md +10 -0
  31. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/concepts/architecture.md +10 -0
  32. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/concepts/partial-stitch.en.md +6 -1
  33. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/concepts/partial-stitch.md +5 -1
  34. coderouter_cli-2.15.1/docs/concepts/stream-truncation.en.md +252 -0
  35. coderouter_cli-2.15.1/docs/concepts/stream-truncation.md +247 -0
  36. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/designs/external-agents-adapter.md +9 -1
  37. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/designs/orchestration-companion.md +3 -0
  38. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/designs/v1.6-auto-router-verification.md +9 -0
  39. coderouter_cli-2.15.1/docs/guides/security.en.md +438 -0
  40. coderouter_cli-2.15.1/docs/guides/security.md +249 -0
  41. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/guides/troubleshooting.en.md +34 -0
  42. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/guides/troubleshooting.md +34 -0
  43. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/guides/usage-guide.en.md +35 -0
  44. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/guides/usage-guide.md +35 -0
  45. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/guides/vscode.md +54 -9
  46. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/openrouter-roster/README.md +4 -3
  47. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/start/quickstart.en.md +18 -31
  48. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/start/quickstart.md +18 -31
  49. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/start/when-do-i-need-coderouter.en.md +3 -5
  50. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/start/when-do-i-need-coderouter.md +3 -5
  51. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/README.md +18 -2
  52. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/providers.cli-session.yaml +12 -5
  53. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/providers.llamacpp-vllm.yaml +2 -1
  54. coderouter_cli-2.15.1/examples/providers.opusplan.yaml +168 -0
  55. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/providers.yaml +3 -1
  56. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/pyproject.toml +1 -1
  57. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_doctor.py +73 -0
  58. coderouter_cli-2.15.1/tests/test_fallback_reason.py +919 -0
  59. coderouter_cli-2.15.1/tests/test_fallback_tool_integrity.py +400 -0
  60. coderouter_cli-2.15.1/tests/test_stream_truncation.py +1444 -0
  61. coderouter_cli-2.14.0/docs/guides/security.en.md +0 -204
  62. coderouter_cli-2.14.0/docs/guides/security.md +0 -126
  63. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/.gitignore +0 -0
  64. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/LICENSE +0 -0
  65. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/__init__.py +0 -0
  66. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/__main__.py +0 -0
  67. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/adapters/__init__.py +0 -0
  68. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/adapters/registry.py +0 -0
  69. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/cli.py +0 -0
  70. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/cli_stats.py +0 -0
  71. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/config/__init__.py +0 -0
  72. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/config/capability_registry.py +0 -0
  73. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/config/env_file.py +0 -0
  74. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/config/loader.py +0 -0
  75. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/cost.py +0 -0
  76. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/credentials.py +0 -0
  77. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/data/__init__.py +0 -0
  78. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/data/model-capabilities.yaml +0 -0
  79. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/doctor_apply.py +0 -0
  80. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/env_security.py +0 -0
  81. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/errors.py +0 -0
  82. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/gguf_introspect.py +0 -0
  83. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/guards/__init__.py +0 -0
  84. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/guards/_fingerprint.py +0 -0
  85. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/guards/backend_health.py +0 -0
  86. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/guards/context_budget.py +0 -0
  87. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/guards/continuous_probe.py +0 -0
  88. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/guards/drift_actions.py +0 -0
  89. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/guards/drift_detection.py +0 -0
  90. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/guards/memory_budget.py +0 -0
  91. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/guards/memory_pressure.py +0 -0
  92. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/guards/self_healing.py +0 -0
  93. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/guards/tool_loop.py +0 -0
  94. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/hardware.py +0 -0
  95. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/ingress/__init__.py +0 -0
  96. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/ingress/app.py +0 -0
  97. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/ingress/dashboard_routes.py +0 -0
  98. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/ingress/launcher_routes.py +0 -0
  99. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/ingress/metrics_routes.py +0 -0
  100. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/ingress/ui_auth.py +0 -0
  101. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/language_tax.py +0 -0
  102. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/launcher_devices.py +0 -0
  103. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/launcher_speculative.py +0 -0
  104. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/launcher_swap.py +0 -0
  105. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/metrics/__init__.py +0 -0
  106. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/output_filters.py +0 -0
  107. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/plugins/__init__.py +0 -0
  108. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/plugins/base.py +0 -0
  109. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/plugins/loader.py +0 -0
  110. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/plugins/registry.py +0 -0
  111. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/py.typed +0 -0
  112. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/rollback.py +0 -0
  113. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/routing/adaptive.py +0 -0
  114. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/routing/auto_router.py +0 -0
  115. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/routing/budget.py +0 -0
  116. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/routing/capability.py +0 -0
  117. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/secret_redaction.py +0 -0
  118. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/state/__init__.py +0 -0
  119. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/state/replay.py +0 -0
  120. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/state/request_log.py +0 -0
  121. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/state/store.py +0 -0
  122. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/state/suggest_rules.py +0 -0
  123. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/token_estimation.py +0 -0
  124. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/token_estimation_accurate.py +0 -0
  125. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/translation/__init__.py +0 -0
  126. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/translation/anthropic.py +0 -0
  127. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/translation/convert.py +0 -0
  128. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/coderouter/translation/tool_repair.py +0 -0
  129. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/assets/dashboard-demo.png +0 -0
  130. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/external-agents.en.md +0 -0
  131. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/external-agents.md +0 -0
  132. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/gguf_dl.en.md +0 -0
  133. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/gguf_dl.md +0 -0
  134. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/hf-ollama-models.en.md +0 -0
  135. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/hf-ollama-models.md +0 -0
  136. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/launcher-quickstart.en.md +0 -0
  137. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/launcher-quickstart.md +0 -0
  138. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/lmstudio-direct.en.md +0 -0
  139. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/lmstudio-direct.md +0 -0
  140. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/backends/verify-ollama-0.23.1.md +0 -0
  141. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/concepts/context-budget.en.md +0 -0
  142. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/concepts/context-budget.md +0 -0
  143. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/concepts/continuous-probing.en.md +0 -0
  144. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/concepts/continuous-probing.md +0 -0
  145. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/concepts/drift-detection.en.md +0 -0
  146. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/concepts/drift-detection.md +0 -0
  147. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/designs/agent-cli-plugin-extraction.md +0 -0
  148. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/designs/launcher-model-swap.md +0 -0
  149. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/designs/launcher-multi-build.md +0 -0
  150. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  151. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/designs/v1.6-auto-router.md +0 -0
  152. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/guides/free-tier-guide.en.md +0 -0
  153. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/guides/free-tier-guide.md +0 -0
  154. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/guides/language-tax.en.md +0 -0
  155. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/guides/language-tax.md +0 -0
  156. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/guides/remote-access.en.md +0 -0
  157. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/guides/remote-access.md +0 -0
  158. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/guides/subagent-routing.en.md +0 -0
  159. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/guides/subagent-routing.md +0 -0
  160. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/low-memory-integration.en.md +0 -0
  161. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/low-memory-integration.md +0 -0
  162. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/openrouter-roster/CHANGES.md +0 -0
  163. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/openrouter-roster/latest.json +0 -0
  164. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/retrospectives/v0.4.md +0 -0
  165. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/retrospectives/v0.5-verify.md +0 -0
  166. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/retrospectives/v0.5.md +0 -0
  167. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/retrospectives/v0.6.md +0 -0
  168. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/retrospectives/v0.7.md +0 -0
  169. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/retrospectives/v1.0-verify.md +0 -0
  170. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/docs/retrospectives/v1.0.md +0 -0
  171. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/.env.example +0 -0
  172. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/providers-agent-cli.yaml +0 -0
  173. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/providers-multiagent.yaml +0 -0
  174. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/providers.context-budget-test.yaml +0 -0
  175. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/providers.nvidia-nim.yaml +0 -0
  176. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/providers.ollama-auto-custom.yaml +0 -0
  177. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/providers.ollama-auto.yaml +0 -0
  178. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/providers.ollama-free-chain.yaml +0 -0
  179. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/providers.raspberrypi.yaml +0 -0
  180. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/examples/providers.swap.yaml +0 -0
  181. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/scripts/demo_traffic.sh +0 -0
  182. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/scripts/openrouter_roster_diff.py +0 -0
  183. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/scripts/smoke_v2_2.sh +0 -0
  184. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/scripts/verify-providers.yaml +0 -0
  185. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/scripts/verify_ollama_0_23.py +0 -0
  186. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/scripts/verify_v0_5.sh +0 -0
  187. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/scripts/verify_v1_0.sh +0 -0
  188. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/__init__.py +0 -0
  189. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/conftest.py +0 -0
  190. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_adapter_anthropic.py +0 -0
  191. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_agent_cli.py +0 -0
  192. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_audit_log.py +0 -0
  193. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_auto_router.py +0 -0
  194. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_auto_router_cjk.py +0 -0
  195. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_backend_health.py +0 -0
  196. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_budget.py +0 -0
  197. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_capability.py +0 -0
  198. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_capability_degraded_payload.py +0 -0
  199. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_capability_registry.py +0 -0
  200. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_capability_registry_cache_control.py +0 -0
  201. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_capability_tool_choice.py +0 -0
  202. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_claude_code_suitability.py +0 -0
  203. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_cli.py +0 -0
  204. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_cli_bind_warning.py +0 -0
  205. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_cli_stats.py +0 -0
  206. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_config.py +0 -0
  207. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_config_cwd_warning.py +0 -0
  208. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_config_shim_actions.py +0 -0
  209. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_context_budget.py +0 -0
  210. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_continuous_probe.py +0 -0
  211. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_credentials.py +0 -0
  212. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_dashboard_endpoint.py +0 -0
  213. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_doctor_apply.py +0 -0
  214. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_doctor_cache_probe.py +0 -0
  215. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_drift_actions.py +0 -0
  216. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_drift_detection.py +0 -0
  217. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_drift_detection_integration.py +0 -0
  218. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_env_file.py +0 -0
  219. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_env_security.py +0 -0
  220. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_errors.py +0 -0
  221. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_examples_yaml.py +0 -0
  222. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_examples_yaml_roundtrip.py +0 -0
  223. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fallback.py +0 -0
  224. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fallback_anthropic.py +0 -0
  225. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fallback_cache_control.py +0 -0
  226. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fallback_cache_observed.py +0 -0
  227. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fallback_cache_strip.py +0 -0
  228. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fallback_empty_response.py +0 -0
  229. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fallback_misconfig_warn.py +0 -0
  230. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fallback_paid_gate.py +0 -0
  231. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fallback_thinking.py +0 -0
  232. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fallback_tool_choice_emulate.py +0 -0
  233. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fix_h1_prometheus_drift.py +0 -0
  234. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fix_h2_h6_translation.py +0 -0
  235. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fix_h3_client_reuse.py +0 -0
  236. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fix_h4_h5_guards.py +0 -0
  237. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fix_h7_probe_restore.py +0 -0
  238. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fix_h8_launcher_auth.py +0 -0
  239. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fix_mA_routing.py +0 -0
  240. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fix_mB_adapters.py +0 -0
  241. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fix_mC_convert.py +0 -0
  242. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fix_mD_logging_metrics.py +0 -0
  243. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fix_mE_config_validation.py +0 -0
  244. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fix_mF_ingress.py +0 -0
  245. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_fix_v213_launcher_hardening.py +0 -0
  246. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_gguf_introspect.py +0 -0
  247. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_guards_tool_loop.py +0 -0
  248. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_hardware.py +0 -0
  249. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_import_hygiene.py +0 -0
  250. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_ingress_anthropic.py +0 -0
  251. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_ingress_count_tokens.py +0 -0
  252. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_ingress_profile.py +0 -0
  253. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_language_tax.py +0 -0
  254. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_language_tax_integration.py +0 -0
  255. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_backend_variants.py +0 -0
  256. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_config_bench.py +0 -0
  257. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_devices.py +0 -0
  258. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_devices_detect.py +0 -0
  259. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_devices_routes.py +0 -0
  260. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_gui_devices.py +0 -0
  261. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_gui_events.py +0 -0
  262. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_gui_readiness.py +0 -0
  263. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_gui_sweep_close.py +0 -0
  264. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_mtp.py +0 -0
  265. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_mtp_fallback.py +0 -0
  266. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_provider_sync.py +0 -0
  267. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_readiness_restart.py +0 -0
  268. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_swap.py +0 -0
  269. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_swap_review.py +0 -0
  270. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_variant_config.py +0 -0
  271. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_variant_gui.py +0 -0
  272. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_variant_routes.py +0 -0
  273. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_launcher_variant_sweep.py +0 -0
  274. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_memory_budget.py +0 -0
  275. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_memory_pressure.py +0 -0
  276. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_metrics_cache.py +0 -0
  277. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_metrics_collector.py +0 -0
  278. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_metrics_cost.py +0 -0
  279. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_metrics_endpoint.py +0 -0
  280. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_metrics_jsonl.py +0 -0
  281. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_metrics_prometheus.py +0 -0
  282. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_metrics_prometheus_cache.py +0 -0
  283. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_models_passthrough.py +0 -0
  284. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_openai_compat.py +0 -0
  285. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_openrouter_roster_diff.py +0 -0
  286. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_output_filters.py +0 -0
  287. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_output_filters_adapters.py +0 -0
  288. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_packaging.py +0 -0
  289. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_partial_stitch.py +0 -0
  290. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_plugin_adapter.py +0 -0
  291. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_plugins_integration.py +0 -0
  292. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_plugins_loader.py +0 -0
  293. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_plugins_registry.py +0 -0
  294. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_reasoning_strip.py +0 -0
  295. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_repair_byte_fallback.py +0 -0
  296. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_request_log.py +0 -0
  297. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_role_normalization.py +0 -0
  298. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_rollback.py +0 -0
  299. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_routing_adaptive.py +0 -0
  300. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_secret_redaction.py +0 -0
  301. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_self_healing.py +0 -0
  302. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_setup_sh.py +0 -0
  303. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_state_store.py +0 -0
  304. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_stop_reason_forward_compat.py +0 -0
  305. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_token_estimation.py +0 -0
  306. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_token_estimation_accurate.py +0 -0
  307. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_tokens_saved_metric.py +0 -0
  308. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_tool_repair.py +0 -0
  309. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_tool_repair_differential.py +0 -0
  310. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_tool_repair_lenient.py +0 -0
  311. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_tool_repair_r4.py +0 -0
  312. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_tool_repair_scanners.py +0 -0
  313. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_toolrepair_bench.py +0 -0
  314. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_translation_anthropic.py +0 -0
  315. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_translation_reverse.py +0 -0
  316. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_ui_auth.py +0 -0
  317. {coderouter_cli-2.14.0 → coderouter_cli-2.15.1}/tests/test_vscode_init.py +0 -0
@@ -9,6 +9,184 @@ are kept verbatim where the Japanese text itself is the subject).
9
9
 
10
10
  ---
11
11
 
12
+ ## [v2.15.1] — 2026-10-24 (docs and defaults for the October 2026 ecosystem)
13
+
14
+ ### Added
15
+
16
+ - **`doctor` names llama.cpp's default-port change.** llama.cpp b11521
17
+ (2026-10-09, ggml-org/llama.cpp#30159) moved `llama-server`'s default
18
+ listen port from 8080 to 9931. When the `auth+basic-chat` probe cannot
19
+ connect to a loopback provider on :8080 or :9931, `doctor` now checks the
20
+ other port's `/health` (1.5 s, IPv4 literal) and, if it answers, says so
21
+ in the transport-error detail with the two possible fixes. The config is
22
+ never rewritten. Other hosts and ports send no extra request.
23
+ - **SBOM on every release.** `release.yml` builds a CycloneDX JSON SBOM
24
+ (`cyclonedx-bom==7.4.0`) from a clean venv holding only the wheel and its
25
+ runtime dependencies, and attaches `coderouter-cli-<version>.cdx.json` to
26
+ the GitHub Release draft. It is written outside `dist/`, so the PyPI
27
+ upload is unchanged.
28
+
29
+ ### Changed
30
+
31
+ - **Codex CLI instructions corrected.** The quickstart (Pattern B), README
32
+ and the "do I need CodeRouter?" guide told users to set
33
+ `wire_api = "chat"`. Codex removed that option in 2026-02 and now fails
34
+ while loading such a config; it speaks only the Responses API, which
35
+ CodeRouter does not serve yet. The docs now say so and give the
36
+ direct-connect workaround (`codex --oss`, or a `wire_api = "responses"`
37
+ provider pointing at `llama-server`).
38
+ - **llama.cpp port notes.** Backend guides and examples keep their explicit
39
+ `--port 8080` (correct on every build) and now explain the 9931 default
40
+ for servers started without `--port`. The Launcher is unaffected: it
41
+ always passes `--port`, so its 8080 default stays.
42
+ - **README: package-name warning.** The PyPI project named `coderouter` is
43
+ a different product; the install section now says to install
44
+ `coderouter-cli`.
45
+ - **README: repair benchmark and release protections.** The tool-call
46
+ repair row links the benchmark and its numbers, and the Security section
47
+ lists the release protections as layers rather than resting on
48
+ Trusted Publishing alone.
49
+
50
+ ### Security
51
+
52
+ - **Lockfile bumps for newly published advisories.** `anyio` 4.13.0 →
53
+ 4.14.2 (PYSEC-2026-4024, PYSEC-2026-4025), `certifi` 2026.2.25 →
54
+ 2026.10.23 (PYSEC-2026-4320), `fsspec` 2026.4.0 → 2026.9.0
55
+ (PYSEC-2026-4217; reached only through the optional `[accuracy]` extra
56
+ via `huggingface-hub`). Found by the CI dependency audit.
57
+
58
+ ---
59
+
60
+ ## [v2.15.0] — 2026-08-21 (fallback explainability + stream truncation detection)
61
+
62
+ ### Added
63
+
64
+ - **Fallback explainability — `X-CodeRouter-Fallback-*` response headers.**
65
+ When a request changes providers, the response now says who it left, who
66
+ took over, and why: `X-CodeRouter-Fallback-From` / `-To` / `-Reason` /
67
+ `-Chain` (multi-hop chains render as `local>ollama>openrouter` with one
68
+ comma-separated reason per departure). Reasons come from a single
69
+ canonical vocabulary in `coderouter/routing/fallback_trace.py` covering
70
+ both chain-resolve filters (`paid-gate`, `budget-exceeded`,
71
+ `memory-pressure`, `backend-unhealthy`, `self-healing-excluded`,
72
+ `unknown-provider`) and runtime attempt failures (`timeout`,
73
+ `rate-limit`, `auth`, `upstream-5xx`, `upstream-4xx`, `connection`,
74
+ `empty-response`, `empty-stream`). The same trail is emitted as
75
+ structured `fallback-occurred` log lines (auditable) and, on the
76
+ Anthropic streaming path, as a trailing `coderouter_fallback` SSE
77
+ metadata event for reasons discovered after the HTTP headers ship.
78
+ Purely additive — a request served by its first provider produces no new
79
+ headers, no SSE trailer and no log lines.
80
+ - **Stream truncation detection — `stream_truncation_action`
81
+ (`off` | `warn` | `error`, default `off`).** An upstream can end its HTTP
82
+ body *cleanly* while the LLM protocol carried inside it is still
83
+ mid-message: no `message_stop` on the Anthropic wire, no `data: [DONE]`
84
+ and no `finish_reason` on the OpenAI wire. llama.cpp slot preemption, an
85
+ `--n-predict` cut-off, a proxy closing an EOF-delimited body, or a local
86
+ server that OOM'd all look exactly like that. Transport breakage
87
+ (timeouts, `httpx.RemoteProtocolError`) was already caught; this is the
88
+ layer the transport cannot see, and until now CodeRouter reported it as a
89
+ *successful* completion — the translation layer's terminator-synthesis
90
+ guards fabricated `stop_reason: end_turn` so the client would not hang,
91
+ which also erased every trace of the cut. The synthesis stays (removing
92
+ it hangs Claude Code); what is new is that the engine gets told. Both
93
+ adapters now track whether a terminator arrived, and under `error` raise
94
+ a retryable `StreamTruncatedError` that joins the *existing* branches: no
95
+ bytes forwarded yet → fall back to the next provider with reason
96
+ `stream-truncated`; bytes already out → `MidStreamError`, which
97
+ `partial_stitch_action: surface` renders as a graceful close carrying a
98
+ `coderouter_partial` event labelled `stream_truncated`. If *every*
99
+ provider in the chain truncates before any byte reached the client, the
100
+ request ends in `NoProvidersAvailableError` (an `overloaded_error` SSE
101
+ frame) rather than flushing a half-written message — see **Fixed** below.
102
+ Because the failure is an ordinary `AdapterError`, the L2/L4/L5/L6
103
+ self-healing guards learn it — a backend that keeps going quiet gets
104
+ demoted by adaptive routing instead of being *preferred* for its fast
105
+ first byte. `warn` measures without changing a single byte of output;
106
+ `off` is byte-for-byte v2.14.0 **on the wire** — see the caveat on
107
+ guard-internal state below. Observability: a `stream-truncation-detected` log
108
+ line (with `wire`, `events_forwarded`, `saw_stream_start`,
109
+ `tool_call_in_flight`), `stream_truncated_total` in `/metrics.json` and
110
+ Prometheus (labelled by provider and by action), and the
111
+ `stream-truncated` reason on the `X-CodeRouter-Fallback-*` headers.
112
+ Terminator matching is deliberately lenient — a `message_delta` carrying
113
+ a `stop_reason`, or a `finish_reason` on any choice, both count — so
114
+ providers that legitimately omit the sentinel are not flagged. On the
115
+ Anthropic streaming path, pre-content fallback additionally needs
116
+ `empty_response_action: fallback`, which is the knob that withholds the
117
+ opening events from the client. Known gap: the terminator synthesis this
118
+ entry refers to lives in `translation/convert.py`, so the one route that
119
+ does not pass through it — `/v1/messages` served by a `kind: anthropic`
120
+ backend, i.e. native passthrough — has never had it. Under `off` a
121
+ truncated native stream still reaches the client unterminated, exactly as
122
+ in v2.14.0; that route is the strongest argument for moving to `warn`.
123
+ Docs: `docs/concepts/stream-truncation.md` / `.en.md`.
124
+ - **Tool-call integrity regression tests across a fallback hop**
125
+ (`tests/test_fallback_tool_integrity.py`): the conversation handed to the
126
+ second provider keeps every `tool_use` / `tool_result` id paired, and the
127
+ response the client receives is still a structurally valid `tool_use`
128
+ block on both the buffered and the streaming path.
129
+ - **Stream truncation tests** (`tests/test_stream_truncation.py`): both
130
+ adapters driven over real SSE bodies (off / warn / error, the
131
+ terminator-leniency false-positive guards, the tool-call-in-flight flag),
132
+ engine-level fallback and `MidStreamError` integration, the ingress
133
+ `coderouter_partial` labelling, and an explicit `off` regression proving
134
+ the default path — including the H6 terminator synthesis — is unchanged.
135
+
136
+ ### Fixed
137
+
138
+ - **Anthropic streaming: a provider that failed before forwarding any byte
139
+ was invisible to the self-healing guards.** Under
140
+ `empty_response_action: fallback` the engine withholds the opening events
141
+ until real content appears, and an `AdapterError` raised in that window
142
+ is handled by swapping providers instead of raising. That branch recorded
143
+ an adaptive failure but never called the L2 memory-pressure / L4 drift /
144
+ L5 backend-health hooks — while `_observe_provider_success` had *already*
145
+ fired the moment the first event landed. The net effect was a backend
146
+ that opens a stream and then dies keeping a clean bill of health, and
147
+ adaptive routing preferring it for its fast first byte. The identical
148
+ failure under `empty_response_action: off` takes the mid-stream path and
149
+ *was* observed, so provider health effectively depended on an unrelated
150
+ knob. Both hooks now run on that branch (and the error joins the `errors`
151
+ list carried by `NoProvidersAvailableError`), matching the mid-stream
152
+ sibling. A stream that terminates cleanly with no content is still not a
153
+ failure — it is a 200 blank, and reaches neither hook.
154
+ **Operational note:** this is a behavior change for anyone already
155
+ running `empty_response_action: fallback` — pre-content failures now
156
+ reach L2/L4/L5, so if `memory_pressure_action: skip` and/or
157
+ `backend_health_action: demote` are also set, skips and demotions can
158
+ become *more frequent* for a backend that was silently dying before real
159
+ content shipped, even though nothing about those knobs' own
160
+ configuration changed. This is independent of `stream_truncation_action`
161
+ — it fires under the default `off` exactly as it does under `warn` /
162
+ `error`, since the branch it lives in is the *pre-existing*
163
+ `empty_response_action: fallback` path, not the new truncation
164
+ detection. See `docs/concepts/stream-truncation.md` / `.en.md` for
165
+ details.
166
+ - **Anthropic streaming: a chain where every provider truncated left the
167
+ client hanging.** On chain exhaustion the engine flushes the last
168
+ buffered preamble so a genuinely empty chain returns a well-formed 200
169
+ blank (`message_start` … `message_stop`). A *truncated* stream's buffer
170
+ has no `message_stop` by definition, so flushing it emitted an SSE
171
+ sequence that never ended — no exception, no error frame, and a client
172
+ waiting forever — and charged `empty_responses_total` for a stream that
173
+ was not empty. The flush is now gated on the buffer actually terminating;
174
+ a truncation-produced buffer falls through to `NoProvidersAvailableError`,
175
+ which the ingress turns into a terminal `overloaded_error` SSE frame. Only
176
+ reachable with `stream_truncation_action: error`, so the default path is
177
+ unchanged, and the empty-chain 200 blank is untouched (including the
178
+ mixed case where a truncated hop is followed by a provider that answered
179
+ with a well-formed empty message — that buffer still gets flushed).
180
+ - **`partial-stitch-surfaced` log shape under the default `off`.** The
181
+ `reason` key added for truncation labelling was attached
182
+ unconditionally, so every `partial_stitch_action: surface` log line
183
+ gained a field even where truncation detection is disabled and the value
184
+ could only ever be `mid_stream_failure`. It is now emitted only when it
185
+ says something new, restoring the v2.14.0 key set. (`/metrics.json` was
186
+ never affected — `MetricsCollector` whitelists the fields it copies.)
187
+
188
+ ---
189
+
12
190
  ## [v2.14.0] — 2026-08-09 (credential hygiene + undo)
13
191
 
14
192
  Five changes that came out of reading `duolahypercho/codex-router` and
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: coderouter-cli
3
- Version: 2.14.0
3
+ Version: 2.15.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
@@ -66,7 +66,7 @@ Description-Content-Type: text/markdown
66
66
  ## 何ができるか — 30 秒で
67
67
 
68
68
  ```
69
- あなたのエージェント (Claude Code / codex / agy)
69
+ あなたのエージェント (Claude Code / agy ほか ※)
70
70
  │
71
71
  ▼
72
72
  ┌─ CodeRouter ─┐
@@ -76,6 +76,8 @@ Description-Content-Type: text/markdown
76
76
  └──────────────┘
77
77
  ```
78
78
 
79
+ ※ 現行の codex CLI は Responses API(`/v1/responses`)専用になったため、CodeRouter 経由ではまだ使えません(対応予定)。直結の代替手順 → [quickstart Pattern B](./docs/start/quickstart.md#pattern-b-codex-cli-で使う)
80
+
79
81
  **やってくれること:**
80
82
 
81
83
  - ローカルモデルが壊した tool calling を Claude Code に届く前に修復する
@@ -118,6 +120,8 @@ uvx --from coderouter-cli coderouter serve --port 8088
118
120
 
119
121
  恒久インストールしたい場合: `uv tool install coderouter-cli`
120
122
 
123
+ > **パッケージ名に注意**: PyPI のパッケージ名は **`coderouter-cli`** です。PyPI の `coderouter`(名前だけ同じ)は別の製品なので、`pip install coderouter` と打たないでください。
124
+
121
125
  ---
122
126
 
123
127
  ## Claude Code で使う
@@ -143,7 +147,7 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
143
147
  | Claude Code + ローカル Ollama で tool calling が壊れる | **必須** — tool 修復 (+ 必要なら wire 変換) |
144
148
  | Claude Code + ローカルで長時間回すと止まる | **必須級** — 6 系統ガード + self-healing |
145
149
  | Ollama v0.14+ / LM Studio にネイティブ直結で動いてる | **便利** — 直結に無い fallback / ガード / 診断を追加 (passthrough で翻訳ゼロ) |
146
- | codex / agy + Ollama 直繋ぎで動いてる | オプション — フォールバックが欲しいなら |
150
+ | codex / agy + Ollama 直繋ぎで動いてる | オプション — フォールバックが欲しいなら(codex は `/v1/responses` 対応後) |
147
151
  | Claude API を直接叩いてて問題ない | 不要 |
148
152
 
149
153
  詳細は → [要否判定ガイド](./docs/start/when-do-i-need-coderouter.md)
@@ -156,7 +160,7 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
156
160
 
157
161
  | 機能 | 何をしてくれるか |
158
162
  |---|---|
159
- | **Tool-call 修復** | ローカルモデルがテキストで吐いた JSON を正しい tool_use ブロックに復元 |
163
+ | **Tool-call 修復** | ローカルモデルがテキストで吐いた JSON を正しい tool_use ブロックに復元。実測は [修復ベンチ](./benchmarks/tool-repair/README.md)(壊れた tool call を出す 4 モデルで成功率 0% → 80〜100%、回帰コーパスで誤修復 0 件。CI でゲート) |
160
164
  | **3 層フォールバック** | ローカル → 無料クラウド → 有料の順に自動切替 |
161
165
  | **出力フィルタ** | `<think>` タグ漏れ、stop marker 漏れ、byte-fallback (`<0xNN>`) を自動除去/修復 |
162
166
  | **Wire 翻訳** | Anthropic 形式 ↔ OpenAI 形式を自動変換 (ネイティブ `/v1/messages` 対応 backend は passthrough で翻訳ゼロ) |
@@ -333,6 +337,14 @@ CodeRouter は backend ルーター層として独立して動きます。`OPENA
333
337
 
334
338
  シークレットは環境変数に置きます。[`docs/security.md`](./docs/guides/security.md) に完全な方針と報告手順があります。
335
339
 
340
+ リリースは多層で守っています。どれか 1 つを根拠に「安全」とは言いません(正規のパイプラインでビルドされた汚染版が出た例があります)。
341
+
342
+ - PyPI へは Trusted Publishing(OIDC)で公開し、長期の API トークンを置かない
343
+ - PyPI 側の公開許可は `release.yml` の `pypi` 環境に限定している
344
+ - タグは事前検査スクリプトを通してから打つ(version・CHANGELOG・main の一致を機械的に確認)
345
+ - 実行時の依存は 5 個に絞っている
346
+ - 各リリースに SBOM(CycloneDX)を添付する
347
+
336
348
  ## License
337
349
 
338
350
  MIT
@@ -21,7 +21,7 @@
21
21
  ## What it does — in 30 seconds
22
22
 
23
23
  ```
24
- Your agent (Claude Code / codex / agy)
24
+ Your agent (Claude Code / agy, etc. *)
25
25
  │
26
26
  ▼
27
27
  ┌─ CodeRouter ──┐
@@ -31,6 +31,8 @@ Your agent (Claude Code / codex / agy)
31
31
  └────────────────┘
32
32
  ```
33
33
 
34
+ \* Current codex CLI speaks only the Responses API (`/v1/responses`), so it cannot go through CodeRouter yet (planned). Direct-connect workaround → [quickstart Pattern B](./docs/start/quickstart.en.md#pattern-b-use-it-with-the-codex-cli)
35
+
34
36
  **What it does for you:**
35
37
 
36
38
  - Repairs broken tool calling from local models before it reaches Claude Code
@@ -55,6 +57,8 @@ uvx --from coderouter-cli coderouter serve --port 8088
55
57
 
56
58
  For a permanent install: `uv tool install coderouter-cli`
57
59
 
60
+ > **Mind the package name**: the PyPI package is **`coderouter-cli`**. The PyPI package named `coderouter` is a different product; don't `pip install coderouter`.
61
+
58
62
  ---
59
63
 
60
64
  ## Use with Claude Code
@@ -79,7 +83,7 @@ That's it. Claude Code works as usual, but your local Ollama is answering behind
79
83
  |---|---|
80
84
  | Claude Code + local Ollama, tool calling breaks | **Yes** — wire translation + tool repair |
81
85
  | Claude Code + local, dies after long sessions | **Helpful** — 6 guards + self-healing |
82
- | codex / agy + Ollama works fine | Optional — if you want fallback |
86
+ | codex / agy + Ollama works fine | Optional — if you want fallback (codex: once `/v1/responses` lands) |
83
87
  | Using Claude API directly, no issues | Not needed |
84
88
 
85
89
  Full decision matrix → [Do I need CodeRouter?](./docs/start/when-do-i-need-coderouter.en.md)
@@ -93,7 +97,7 @@ Full decision matrix → [Do I need CodeRouter?](./docs/start/when-do-i-need-cod
93
97
  | Feature | What it does |
94
98
  |---|---|
95
99
  | **Wire translation** | Claude Code (Anthropic format) ↔ Ollama (OpenAI format) auto-converted |
96
- | **Tool-call repair** | JSON that local models emit as plain text → valid tool_use blocks |
100
+ | **Tool-call repair** | JSON that local models emit as plain text → valid tool_use blocks. Measured in the [repair benchmark](./benchmarks/tool-repair/README.md): four models that break tool calls go from 0% to 80–100% success; zero false repairs on the regression corpus, gated in CI |
97
101
  | **3-tier fallback** | Local → free cloud → paid, automatic switching |
98
102
  | **Output filters** | Strips leaked `<think>` tags, stop markers, XML tool tags |
99
103
 
@@ -268,6 +272,14 @@ CodeRouter runs as an independent backend router layer. Point any project's `OPE
268
272
 
269
273
  Secrets go in env vars, not config files. See [`docs/security.en.md`](./docs/guides/security.en.md) for the full policy and reporting instructions.
270
274
 
275
+ Releases are protected in layers. No single one of these is offered as proof of safety (tampered builds have shipped from legitimate pipelines before).
276
+
277
+ - Published to PyPI via Trusted Publishing (OIDC); no long-lived API token exists
278
+ - PyPI accepts uploads only from `release.yml` running in the `pypi` environment
279
+ - Tags are cut only after a pre-flight script checks version, CHANGELOG and main
280
+ - Five runtime dependencies
281
+ - An SBOM (CycloneDX) is attached to every release
282
+
271
283
  ## License
272
284
 
273
285
  MIT
@@ -21,7 +21,7 @@
21
21
  ## 何ができるか — 30 秒で
22
22
 
23
23
  ```
24
- あなたのエージェント (Claude Code / codex / agy)
24
+ あなたのエージェント (Claude Code / agy ほか ※)
25
25
  │
26
26
  ▼
27
27
  ┌─ CodeRouter ─┐
@@ -31,6 +31,8 @@
31
31
  └──────────────┘
32
32
  ```
33
33
 
34
+ ※ 現行の codex CLI は Responses API(`/v1/responses`)専用になったため、CodeRouter 経由ではまだ使えません(対応予定)。直結の代替手順 → [quickstart Pattern B](./docs/start/quickstart.md#pattern-b-codex-cli-で使う)
35
+
34
36
  **やってくれること:**
35
37
 
36
38
  - ローカルモデルが壊した tool calling を Claude Code に届く前に修復する
@@ -73,6 +75,8 @@ uvx --from coderouter-cli coderouter serve --port 8088
73
75
 
74
76
  恒久インストールしたい場合: `uv tool install coderouter-cli`
75
77
 
78
+ > **パッケージ名に注意**: PyPI のパッケージ名は **`coderouter-cli`** です。PyPI の `coderouter`(名前だけ同じ)は別の製品なので、`pip install coderouter` と打たないでください。
79
+
76
80
  ---
77
81
 
78
82
  ## Claude Code で使う
@@ -98,7 +102,7 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
98
102
  | Claude Code + ローカル Ollama で tool calling が壊れる | **必須** — tool 修復 (+ 必要なら wire 変換) |
99
103
  | Claude Code + ローカルで長時間回すと止まる | **必須級** — 6 系統ガード + self-healing |
100
104
  | Ollama v0.14+ / LM Studio にネイティブ直結で動いてる | **便利** — 直結に無い fallback / ガード / 診断を追加 (passthrough で翻訳ゼロ) |
101
- | codex / agy + Ollama 直繋ぎで動いてる | オプション — フォールバックが欲しいなら |
105
+ | codex / agy + Ollama 直繋ぎで動いてる | オプション — フォールバックが欲しいなら(codex は `/v1/responses` 対応後) |
102
106
  | Claude API を直接叩いてて問題ない | 不要 |
103
107
 
104
108
  詳細は → [要否判定ガイド](./docs/start/when-do-i-need-coderouter.md)
@@ -111,7 +115,7 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
111
115
 
112
116
  | 機能 | 何をしてくれるか |
113
117
  |---|---|
114
- | **Tool-call 修復** | ローカルモデルがテキストで吐いた JSON を正しい tool_use ブロックに復元 |
118
+ | **Tool-call 修復** | ローカルモデルがテキストで吐いた JSON を正しい tool_use ブロックに復元。実測は [修復ベンチ](./benchmarks/tool-repair/README.md)(壊れた tool call を出す 4 モデルで成功率 0% → 80〜100%、回帰コーパスで誤修復 0 件。CI でゲート) |
115
119
  | **3 層フォールバック** | ローカル → 無料クラウド → 有料の順に自動切替 |
116
120
  | **出力フィルタ** | `<think>` タグ漏れ、stop marker 漏れ、byte-fallback (`<0xNN>`) を自動除去/修復 |
117
121
  | **Wire 翻訳** | Anthropic 形式 ↔ OpenAI 形式を自動変換 (ネイティブ `/v1/messages` 対応 backend は passthrough で翻訳ゼロ) |
@@ -288,6 +292,14 @@ CodeRouter は backend ルーター層として独立して動きます。`OPENA
288
292
 
289
293
  シークレットは環境変数に置きます。[`docs/security.md`](./docs/guides/security.md) に完全な方針と報告手順があります。
290
294
 
295
+ リリースは多層で守っています。どれか 1 つを根拠に「安全」とは言いません(正規のパイプラインでビルドされた汚染版が出た例があります)。
296
+
297
+ - PyPI へは Trusted Publishing(OIDC)で公開し、長期の API トークンを置かない
298
+ - PyPI 側の公開許可は `release.yml` の `pypi` 環境に限定している
299
+ - タグは事前検査スクリプトを通してから打つ(version・CHANGELOG・main の一致を機械的に確認)
300
+ - 実行時の依存は 5 個に絞っている
301
+ - 各リリースに SBOM(CycloneDX)を添付する
302
+
291
303
  ## License
292
304
 
293
305
  MIT
@@ -44,9 +44,14 @@ from coderouter.adapters.base import (
44
44
  ChatResponse,
45
45
  ProviderCallOverrides,
46
46
  StreamChunk,
47
+ StreamTruncatedError,
47
48
  )
48
49
  from coderouter.credentials import resolve_provider_credential
49
- from coderouter.logging import get_logger, log_output_filter_applied
50
+ from coderouter.logging import (
51
+ get_logger,
52
+ log_output_filter_applied,
53
+ log_stream_truncation_detected,
54
+ )
50
55
  from coderouter.output_filters import OutputFilterChain
51
56
  from coderouter.translation.anthropic import (
52
57
  AnthropicRequest,
@@ -74,6 +79,74 @@ _RETRYABLE_STATUSES = {404, 408, 425, 429, 500, 502, 503, 504}
74
79
  _DEFAULT_ANTHROPIC_VERSION = "2023-06-01"
75
80
 
76
81
 
82
+ class _AnthropicTerminatorTracker:
83
+ """v2.15.0: watch an Anthropic SSE event stream for its terminator.
84
+
85
+ The parse loop in :meth:`AnthropicAdapter.stream_anthropic` used to be
86
+ stateless across events — ``current_event`` was reset on every blank
87
+ line and nothing remembered whether the stream had ended *properly*.
88
+ That made "upstream sent 30 deltas and hung up" indistinguishable from
89
+ "upstream sent the whole message". This tracker is the missing memory.
90
+
91
+ What counts as a terminator (deliberately generous, to keep the false
92
+ positive rate down — see ``stream_truncation_action`` in
93
+ ``config/schemas.py``):
94
+
95
+ * ``message_stop`` — the canonical Anthropic terminator.
96
+ * ``message_delta`` carrying a non-null ``stop_reason`` — the message is
97
+ semantically complete even if the provider omits ``message_stop``.
98
+ Some Anthropic-compatible local servers do exactly that, and their
99
+ content is *not* truncated, so flagging them would be a false positive.
100
+ * a top-level ``error`` event — the upstream ended the stream on purpose
101
+ and the frame is forwarded to the client verbatim (current behavior).
102
+ Reporting truncation on top would double-report one failure.
103
+
104
+ ``open_block_type`` tracks the currently-open content block so a stream
105
+ cut in the middle of a ``tool_use`` block's ``input_json_delta`` can be
106
+ reported as such: that is the case where the translation layer's
107
+ terminator synthesis closes a block whose argument JSON is provably
108
+ incomplete.
109
+ """
110
+
111
+ __slots__ = ("events", "open_block_type", "saw_start", "saw_terminator")
112
+
113
+ def __init__(self) -> None:
114
+ """Start with nothing observed."""
115
+ self.saw_terminator: bool = False
116
+ self.saw_start: bool = False
117
+ self.events: int = 0
118
+ self.open_block_type: str | None = None
119
+
120
+ def observe(self, event_type: str, data: Any) -> None:
121
+ """Fold one parsed SSE event into the tracked state.
122
+
123
+ ``data`` is whatever ``json.loads`` produced for the block, so it is
124
+ not guaranteed to be a mapping; non-dict payloads are treated as
125
+ empty rather than raising inside a streaming hot path.
126
+ """
127
+ self.events += 1
128
+ if not isinstance(data, dict):
129
+ data = {}
130
+ if event_type == "message_start":
131
+ self.saw_start = True
132
+ elif event_type == "content_block_start":
133
+ block = data.get("content_block")
134
+ self.open_block_type = block.get("type") if isinstance(block, dict) else None
135
+ elif event_type == "content_block_stop":
136
+ self.open_block_type = None
137
+ elif event_type == "message_delta":
138
+ delta = data.get("delta")
139
+ if isinstance(delta, dict) and delta.get("stop_reason"):
140
+ self.saw_terminator = True
141
+ elif event_type in {"message_stop", "error"}:
142
+ self.saw_terminator = True
143
+
144
+ @property
145
+ def tool_call_in_flight(self) -> bool:
146
+ """True when a ``tool_use`` block was still open at end-of-stream."""
147
+ return self.open_block_type == "tool_use"
148
+
149
+
77
150
  def anthropic_messages_url(base_url: str) -> str:
78
151
  """Resolve the ``/v1/messages`` endpoint URL from a ``base_url``.
79
152
 
@@ -475,6 +548,12 @@ class AnthropicAdapter(BaseAdapter):
475
548
  filter_chains: dict[int, OutputFilterChain] = {}
476
549
  logged_flag: list[bool] = [False]
477
550
 
551
+ # v2.15.0 (stream-truncation): remember whether the upstream ever
552
+ # terminated its message. Resolved once per call — the knob is a
553
+ # profile-level setting threaded in through ProviderCallOverrides.
554
+ truncation_action = self.effective_stream_truncation_action(overrides)
555
+ terminator = _AnthropicTerminatorTracker()
556
+
478
557
  try:
479
558
  # H3: stream over the shared client (pool / keep-alive / TLS
480
559
  # reuse). Only the ``stream(...)`` context is scoped here; the
@@ -514,6 +593,7 @@ class AnthropicAdapter(BaseAdapter):
514
593
  current_event = None
515
594
  data_lines = []
516
595
  continue
596
+ terminator.observe(current_event, data_obj)
517
597
  for out_event in self._process_stream_event_for_filters(
518
598
  AnthropicStreamEvent(type=current_event, data=data_obj),
519
599
  chains=filter_chains,
@@ -542,12 +622,37 @@ class AnthropicAdapter(BaseAdapter):
542
622
  except json.JSONDecodeError:
543
623
  data_obj = None
544
624
  if data_obj is not None:
625
+ terminator.observe(current_event, data_obj)
545
626
  for out_event in self._process_stream_event_for_filters(
546
627
  AnthropicStreamEvent(type=current_event, data=data_obj),
547
628
  chains=filter_chains,
548
629
  logged_flag=logged_flag,
549
630
  ):
550
631
  yield out_event
632
+
633
+ # v2.15.0 (stream-truncation): the upstream line iterator is
634
+ # exhausted. Reaching here with no terminator observed means the
635
+ # HTTP body ended cleanly while the Anthropic message was still
636
+ # open — the exact case transport-level error handling below
637
+ # cannot see (``httpx.RemoteProtocolError`` and timeouts already
638
+ # raise). Under ``off`` this block is inert, which is what keeps
639
+ # the default byte-for-byte identical to v2.14.0.
640
+ if truncation_action != "off" and not terminator.saw_terminator:
641
+ log_stream_truncation_detected(
642
+ logger,
643
+ self.name,
644
+ action=truncation_action,
645
+ wire="anthropic",
646
+ events_forwarded=terminator.events,
647
+ saw_stream_start=terminator.saw_start,
648
+ tool_call_in_flight=terminator.tool_call_in_flight,
649
+ )
650
+ if truncation_action == "error":
651
+ raise StreamTruncatedError(
652
+ "upstream stream truncated (no message_stop terminator)",
653
+ provider=self.name,
654
+ tool_call_in_flight=terminator.tool_call_in_flight,
655
+ )
551
656
  except httpx.TimeoutException as exc:
552
657
  raise AdapterError(
553
658
  f"timeout streaming from {url}", provider=self.name, retryable=True
@@ -137,6 +137,49 @@ class AdapterError(CodeRouterError):
137
137
  return f"[{self.provider}{sc}] {super().__str__()}"
138
138
 
139
139
 
140
+ # v2.15.0 (stream-truncation): raised by an adapter when the upstream SSE
141
+ # stream ended *without* its protocol terminator (``message_stop`` on the
142
+ # Anthropic wire, ``data: [DONE]`` / a ``finish_reason`` on the OpenAI wire).
143
+ #
144
+ # HTTP-level breakage (timeout, ``httpx.RemoteProtocolError``) already became a
145
+ # plain ``AdapterError`` in both adapters. This subclass covers the layer
146
+ # mismatch the transport cannot see: the HTTP body ended cleanly, but the *LLM
147
+ # protocol* carried inside it was still mid-message.
148
+ #
149
+ # It subclasses ``AdapterError`` on purpose — every existing engine branch
150
+ # (``except AdapterError``) keeps working untouched, and the L2/L4/L5/L6 guards
151
+ # learn from the failure automatically. The subclass only adds identity, so
152
+ # ``classify_adapter_error`` can label the hop ``stream-truncated`` instead of
153
+ # the generic ``upstream-error``.
154
+ #
155
+ # Raised only when the active profile sets ``stream_truncation_action: error``.
156
+ # Under ``off`` (the default) and ``warn`` the adapter returns normally and the
157
+ # legacy terminator-synthesis path in ``coderouter.translation.convert`` (H6 /
158
+ # M9) runs byte-for-byte as before.
159
+ class StreamTruncatedError(AdapterError):
160
+ """Upstream SSE stream ended without its protocol terminator event."""
161
+
162
+ def __init__(
163
+ self,
164
+ message: str,
165
+ *,
166
+ provider: str,
167
+ tool_call_in_flight: bool = False,
168
+ ) -> None:
169
+ """Construct a truncation error.
170
+
171
+ Args:
172
+ message: Human-readable reason. Never contains upstream body text.
173
+ provider: The ``ProviderConfig.name`` whose stream was cut.
174
+ tool_call_in_flight: True when the stream was cut while a
175
+ ``tool_use`` / ``tool_calls`` block was still open, i.e. the
176
+ accumulated argument JSON is certainly incomplete. Carried for
177
+ observability only — it never changes control flow.
178
+ """
179
+ super().__init__(message, provider=provider, status_code=None, retryable=True)
180
+ self.tool_call_in_flight = tool_call_in_flight
181
+
182
+
140
183
  # v0.6-B: per-call overrides resolved from the active profile. The engine
141
184
  # builds one instance per request (since a profile is invariant across its
142
185
  # chain) and threads it through every adapter call on that chain. Adapters
@@ -150,6 +193,13 @@ class AdapterError(CodeRouterError):
150
193
  # - ``append_system_prompt=""`` is a meaningful explicit value: "for
151
194
  # this profile, clear the provider's directive". The adapter must
152
195
  # distinguish ``None`` (no override) from ``""`` (override-to-empty).
196
+ #
197
+ # v2.15.0 (stream-truncation): ``stream_truncation_action`` rides the same
198
+ # channel. It is a *profile*-level knob but has to be read inside the adapter's
199
+ # SSE parse loop, and ``ProviderCallOverrides`` is the only object the engine
200
+ # already threads into every adapter call. Its default of ``"off"`` keeps
201
+ # ``ProviderCallOverrides()`` (and every legacy call site that passes nothing)
202
+ # on the pre-v2.15.0 behavior.
153
203
  class ProviderCallOverrides(BaseModel):
154
204
  """Per-call provider overrides, resolved from the active profile."""
155
205
 
@@ -157,6 +207,7 @@ class ProviderCallOverrides(BaseModel):
157
207
 
158
208
  timeout_s: float | None = None
159
209
  append_system_prompt: str | None = None
210
+ stream_truncation_action: Literal["off", "warn", "error"] = "off"
160
211
 
161
212
 
162
213
  class BaseAdapter(ABC):
@@ -236,6 +287,20 @@ class BaseAdapter(ABC):
236
287
  return overrides.append_system_prompt or None
237
288
  return self.config.append_system_prompt
238
289
 
290
+ def effective_stream_truncation_action(
291
+ self, overrides: ProviderCallOverrides | None
292
+ ) -> str:
293
+ """v2.15.0: resolve ``stream_truncation_action`` for this call.
294
+
295
+ There is no per-provider counterpart — the knob lives on the profile
296
+ only — so this is just a null-safe read. ``None`` overrides (legacy
297
+ call sites, unit tests constructing adapters directly) resolve to
298
+ ``"off"``, which is the pre-v2.15.0 behavior.
299
+ """
300
+ if overrides is None:
301
+ return "off"
302
+ return overrides.stream_truncation_action
303
+
239
304
  @abstractmethod
240
305
  async def healthcheck(self) -> bool:
241
306
  """Lightweight check that the upstream is reachable. Return True if healthy."""