coderouter-cli 2.7.2__tar.gz → 2.7.4__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 (238) hide show
  1. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/CHANGELOG.md +83 -0
  2. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/PKG-INFO +3 -1
  3. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/README.en.md +2 -0
  4. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/README.md +2 -0
  5. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/config/schemas.py +51 -0
  6. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/launcher_routes.py +51 -1
  7. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/openai_routes.py +85 -8
  8. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/logging.py +40 -0
  9. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/metrics/collector.py +29 -0
  10. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/routing/fallback.py +290 -5
  11. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/launcher.md +25 -0
  12. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/pyproject.toml +1 -1
  13. coderouter_cli-2.7.4/tests/test_fallback_empty_response.py +561 -0
  14. coderouter_cli-2.7.4/tests/test_launcher_provider_sync.py +124 -0
  15. coderouter_cli-2.7.4/tests/test_models_passthrough.py +144 -0
  16. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/.gitignore +0 -0
  17. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/LICENSE +0 -0
  18. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/__init__.py +0 -0
  19. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/__main__.py +0 -0
  20. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/adapters/__init__.py +0 -0
  21. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/adapters/anthropic_native.py +0 -0
  22. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/adapters/base.py +0 -0
  23. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/adapters/openai_compat.py +0 -0
  24. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/adapters/registry.py +0 -0
  25. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/cli.py +0 -0
  26. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/cli_stats.py +0 -0
  27. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/config/__init__.py +0 -0
  28. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/config/capability_registry.py +0 -0
  29. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/config/env_file.py +0 -0
  30. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/config/loader.py +0 -0
  31. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/cost.py +0 -0
  32. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/data/__init__.py +0 -0
  33. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/data/model-capabilities.yaml +0 -0
  34. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/doctor.py +0 -0
  35. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/doctor_apply.py +0 -0
  36. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/env_security.py +0 -0
  37. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/errors.py +0 -0
  38. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/gguf_introspect.py +0 -0
  39. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/__init__.py +0 -0
  40. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/_fingerprint.py +0 -0
  41. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/backend_health.py +0 -0
  42. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/context_budget.py +0 -0
  43. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/continuous_probe.py +0 -0
  44. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/drift_actions.py +0 -0
  45. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/drift_detection.py +0 -0
  46. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/memory_budget.py +0 -0
  47. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/memory_pressure.py +0 -0
  48. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/self_healing.py +0 -0
  49. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/tool_loop.py +0 -0
  50. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/hardware.py +0 -0
  51. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/__init__.py +0 -0
  52. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/anthropic_routes.py +0 -0
  53. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/app.py +0 -0
  54. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/dashboard_routes.py +0 -0
  55. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/metrics_routes.py +0 -0
  56. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/language_tax.py +0 -0
  57. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/metrics/__init__.py +0 -0
  58. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/metrics/prometheus.py +0 -0
  59. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/output_filters.py +0 -0
  60. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/plugins/__init__.py +0 -0
  61. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/plugins/base.py +0 -0
  62. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/plugins/loader.py +0 -0
  63. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/plugins/registry.py +0 -0
  64. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/routing/__init__.py +0 -0
  65. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/routing/adaptive.py +0 -0
  66. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/routing/auto_router.py +0 -0
  67. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/routing/budget.py +0 -0
  68. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/routing/capability.py +0 -0
  69. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/state/__init__.py +0 -0
  70. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/state/audit_log.py +0 -0
  71. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/state/replay.py +0 -0
  72. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/state/request_log.py +0 -0
  73. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/state/store.py +0 -0
  74. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/state/suggest_rules.py +0 -0
  75. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/token_estimation.py +0 -0
  76. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/token_estimation_accurate.py +0 -0
  77. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/translation/__init__.py +0 -0
  78. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/translation/anthropic.py +0 -0
  79. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/translation/convert.py +0 -0
  80. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/translation/tool_repair.py +0 -0
  81. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/README.md +0 -0
  82. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/assets/dashboard-demo.png +0 -0
  83. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
  84. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/gguf_dl.md +0 -0
  85. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/hf-ollama-models.md +0 -0
  86. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/install-backends.en.md +0 -0
  87. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/install-backends.md +0 -0
  88. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/launcher-quickstart.md +0 -0
  89. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/llamacpp-direct.en.md +0 -0
  90. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/llamacpp-direct.md +0 -0
  91. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/lmstudio-direct.en.md +0 -0
  92. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/lmstudio-direct.md +0 -0
  93. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/verify-ollama-0.23.1.md +0 -0
  94. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/concepts/architecture.md +0 -0
  95. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/concepts/context-budget.md +0 -0
  96. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/concepts/continuous-probing.md +0 -0
  97. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/concepts/drift-detection.md +0 -0
  98. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/concepts/partial-stitch.md +0 -0
  99. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  100. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/designs/v1.6-auto-router-verification.md +0 -0
  101. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/designs/v1.6-auto-router.md +0 -0
  102. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/free-tier-guide.en.md +0 -0
  103. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/free-tier-guide.md +0 -0
  104. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/language-tax.en.md +0 -0
  105. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/language-tax.md +0 -0
  106. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/security.en.md +0 -0
  107. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/security.md +0 -0
  108. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/troubleshooting.en.md +0 -0
  109. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/troubleshooting.md +0 -0
  110. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/usage-guide.en.md +0 -0
  111. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/usage-guide.md +0 -0
  112. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/low-memory-integration.md +0 -0
  113. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/openrouter-roster/CHANGES.md +0 -0
  114. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/openrouter-roster/README.md +0 -0
  115. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/openrouter-roster/latest.json +0 -0
  116. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v0.4.md +0 -0
  117. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v0.5-verify.md +0 -0
  118. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v0.5.md +0 -0
  119. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v0.6.md +0 -0
  120. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v0.7.md +0 -0
  121. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v1.0-verify.md +0 -0
  122. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v1.0.md +0 -0
  123. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/start/quickstart.en.md +0 -0
  124. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/start/quickstart.md +0 -0
  125. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/start/when-do-i-need-coderouter.en.md +0 -0
  126. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/start/when-do-i-need-coderouter.md +0 -0
  127. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/.env.example +0 -0
  128. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/README.md +0 -0
  129. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers-multiagent.yaml +0 -0
  130. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.context-budget-test.yaml +0 -0
  131. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.llamacpp-vllm.yaml +0 -0
  132. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.nvidia-nim.yaml +0 -0
  133. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.ollama-auto-custom.yaml +0 -0
  134. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.ollama-auto.yaml +0 -0
  135. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.ollama-free-chain.yaml +0 -0
  136. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.raspberrypi.yaml +0 -0
  137. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.yaml +0 -0
  138. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/demo_traffic.sh +0 -0
  139. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/openrouter_roster_diff.py +0 -0
  140. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/smoke_v2_2.sh +0 -0
  141. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/verify-providers.yaml +0 -0
  142. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/verify_ollama_0_23.py +0 -0
  143. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/verify_v0_5.sh +0 -0
  144. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/verify_v1_0.sh +0 -0
  145. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/__init__.py +0 -0
  146. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/conftest.py +0 -0
  147. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_adapter_anthropic.py +0 -0
  148. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_audit_log.py +0 -0
  149. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_auto_router.py +0 -0
  150. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_auto_router_cjk.py +0 -0
  151. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_backend_health.py +0 -0
  152. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_budget.py +0 -0
  153. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_capability.py +0 -0
  154. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_capability_degraded_payload.py +0 -0
  155. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_capability_registry.py +0 -0
  156. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_capability_registry_cache_control.py +0 -0
  157. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_capability_tool_choice.py +0 -0
  158. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_claude_code_suitability.py +0 -0
  159. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_cli.py +0 -0
  160. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_cli_stats.py +0 -0
  161. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_config.py +0 -0
  162. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_config_shim_actions.py +0 -0
  163. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_context_budget.py +0 -0
  164. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_continuous_probe.py +0 -0
  165. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_dashboard_endpoint.py +0 -0
  166. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_doctor.py +0 -0
  167. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_doctor_apply.py +0 -0
  168. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_doctor_cache_probe.py +0 -0
  169. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_drift_actions.py +0 -0
  170. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_drift_detection.py +0 -0
  171. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_drift_detection_integration.py +0 -0
  172. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_env_file.py +0 -0
  173. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_env_security.py +0 -0
  174. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_errors.py +0 -0
  175. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_examples_yaml.py +0 -0
  176. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback.py +0 -0
  177. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_anthropic.py +0 -0
  178. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_cache_control.py +0 -0
  179. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_cache_observed.py +0 -0
  180. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_cache_strip.py +0 -0
  181. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_misconfig_warn.py +0 -0
  182. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_paid_gate.py +0 -0
  183. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_thinking.py +0 -0
  184. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_tool_choice_emulate.py +0 -0
  185. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_h1_prometheus_drift.py +0 -0
  186. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_h2_h6_translation.py +0 -0
  187. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_h3_client_reuse.py +0 -0
  188. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_h4_h5_guards.py +0 -0
  189. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_h7_probe_restore.py +0 -0
  190. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_h8_launcher_auth.py +0 -0
  191. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_mA_routing.py +0 -0
  192. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_mB_adapters.py +0 -0
  193. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_mC_convert.py +0 -0
  194. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_mD_logging_metrics.py +0 -0
  195. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_mE_config_validation.py +0 -0
  196. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_mF_ingress.py +0 -0
  197. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_gguf_introspect.py +0 -0
  198. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_guards_tool_loop.py +0 -0
  199. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_hardware.py +0 -0
  200. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_ingress_anthropic.py +0 -0
  201. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_ingress_count_tokens.py +0 -0
  202. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_ingress_profile.py +0 -0
  203. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_language_tax.py +0 -0
  204. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_language_tax_integration.py +0 -0
  205. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_memory_budget.py +0 -0
  206. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_memory_pressure.py +0 -0
  207. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_cache.py +0 -0
  208. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_collector.py +0 -0
  209. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_cost.py +0 -0
  210. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_endpoint.py +0 -0
  211. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_jsonl.py +0 -0
  212. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_prometheus.py +0 -0
  213. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_prometheus_cache.py +0 -0
  214. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_openai_compat.py +0 -0
  215. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_openrouter_roster_diff.py +0 -0
  216. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_output_filters.py +0 -0
  217. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_output_filters_adapters.py +0 -0
  218. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_partial_stitch.py +0 -0
  219. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_plugins_integration.py +0 -0
  220. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_plugins_loader.py +0 -0
  221. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_plugins_registry.py +0 -0
  222. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_reasoning_strip.py +0 -0
  223. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_repair_byte_fallback.py +0 -0
  224. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_request_log.py +0 -0
  225. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_role_normalization.py +0 -0
  226. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_routing_adaptive.py +0 -0
  227. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_self_healing.py +0 -0
  228. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_setup_sh.py +0 -0
  229. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_state_store.py +0 -0
  230. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_token_estimation.py +0 -0
  231. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_token_estimation_accurate.py +0 -0
  232. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_tokens_saved_metric.py +0 -0
  233. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_tool_repair.py +0 -0
  234. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_tool_repair_lenient.py +0 -0
  235. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_tool_repair_r4.py +0 -0
  236. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_toolrepair_bench.py +0 -0
  237. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_translation_anthropic.py +0 -0
  238. {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_translation_reverse.py +0 -0
@@ -6,6 +6,89 @@ versioning follows [SemVer](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [v2.7.4] — 2026-07-06 (Launcher provider auto-sync + /v1/models passthrough)
10
+
11
+ Two PRs closing the "launcher started it, but nothing can route to it /
12
+ nothing can *name* it" gap, driven by a real field failure (launcher on
13
+ port 8085, providers.yaml pointing at 8080; external benchmark reports
14
+ indistinguishable across loaded GGUFs). **PR #48** (models passthrough)
15
+ and **PR #49** (launcher auto-sync). Fully backward compatible, no new
16
+ dependencies. Verified end-to-end on real hardware (NucBox EVO-X2,
17
+ llama.cpp + omnicoder-9b): zero-config launch → route via
18
+ `X-CodeRouter-Profile: launcher` → response carries the real GGUF name.
19
+
20
+ ### Added
21
+
22
+ - **`/v1/models` upstream passthrough** for empty-model providers.
23
+ A provider with `model: ""` (the launcher/llama-server pattern — the
24
+ upstream decides what is loaded) used to be listed by its static
25
+ provider name only. Now `/v1/models` queries the upstream's own
26
+ `/models` (2s timeout, 30s TTL cache) and surfaces the real loaded
27
+ model id(s) as `owned_by: coderouter/<provider>`. Any failure falls
28
+ back to the historic provider-name entry; providers with a configured
29
+ model keep the exact previous shape. External benchmarks (e.g.
30
+ SWE-bench harnesses with `model: auto`) can now tell GGUF swaps apart
31
+ without config edits. (PR #48)
32
+ - **Launcher provider auto-sync.** A backend started from the embedded
33
+ launcher (`/api/launcher/start`) is auto-registered as a routable
34
+ provider named `launcher-<backend>-<port>` (restart on the same port
35
+ replaces, never duplicates) and placed at the FRONT of an on-demand
36
+ `launcher` profile. `default_profile` is never touched — routing is an
37
+ explicit opt-in via `X-CodeRouter-Profile: launcher` or body
38
+ `profile`. Registration is deliberately **in-memory only** (a YAML
39
+ rewrite would destroy operator comments; provider and process share
40
+ the server's lifetime). Sync failures never fail the start; the
41
+ result rides on the start response as `provider_sync`. New engine
42
+ API: `FallbackEngine.register_provider()`. (PR #49)
43
+
44
+ ### Docs
45
+
46
+ - README (JA/EN) launcher feature table + `docs/backends/launcher.md`:
47
+ auto-sync semantics, verification curl recipes, and the desktop-GUI
48
+ caveat (launcher_gui.py runs out-of-process and is not synced).
49
+
50
+ ---
51
+
52
+ ## [v2.7.3] — 2026-07-05 (Per-request empty-response fallback + L3 bench evidence)
53
+
54
+ Two PRs: **PR #45** (`empty_response_action` — the fallback leg of the
55
+ three-tier tool-call reliability story) and **PR #46** (L3 benchmark
56
+ refresh: model matrix, repairer-version A/B archives, results). Fully
57
+ backward compatible: new knob defaults to `off`, no breaking config
58
+ changes, no new dependencies.
59
+
60
+ ### Added
61
+
62
+ - **`FallbackChain.empty_response_action: off | warn | fallback`**
63
+ (default `off`). With `fallback`, a 200 response that is *content-empty*
64
+ (no `tool_use`, no non-whitespace text; thinking-only counts as empty) is
65
+ re-dispatched in-flight to the next provider in the chain. Non-streaming
66
+ responses are judged on the finalized object; streaming buffers events
67
+ until the first real content token and discards an unobserved stream.
68
+ Emptiness is judged on content, not `usage.output_tokens` (unreliable on
69
+ some backends). Motivation: gemma4:26b returns blank 200s on
70
+ `no_tool_temptation`-type prompts (20/20 at temperature 0, direct AND via
71
+ router) — no text exists for the repair layer to fix, and the windowed
72
+ drift guard `empty_response_rate` cannot rescue a single blank turn
73
+ in-flight. 21 new tests. (PR #45)
74
+ - **Benchmark refresh**: full L3 model matrix in
75
+ `benchmarks/tool-repair/providers.bench.yaml` (llama3.2/3.1, mistral,
76
+ qwen2.5-coder:1.5b, phi4-mini + originals, `bench-gemma-chain` enabled),
77
+ one-shot matrix runner `bench_l3_p1.sh`, latest live results plus
78
+ repairer-version A/B snapshots under `results/archive-v2.7.{0,1}/`.
79
+ (PR #46)
80
+
81
+ ### Results
82
+
83
+ - Live chain validation (M3 Max, temperature 0, 100 requests, zero
84
+ errors): gemma4:26b → qwen3-coder:30b chain **80% → 100%** native —
85
+ all 20 blank turns rescued.
86
+ - Three-tier story now fully measured: repair rescues broken-but-present
87
+ calls (qwen2.5×2 / mistral 0→100, phi4 +60pt), healthy models pass
88
+ through undegraded, and fallback closes what repair cannot touch.
89
+
90
+ ---
91
+
9
92
  ## [v2.7.2] — 2026-07-05 (R4 tool-call repair: nested-XML / JSON-envelope / call-syntax forms)
10
93
 
11
94
  Single feature PR **#43**, driven by the 2026-07-05 L3 live benchmark
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.7.2
3
+ Version: 2.7.4
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
@@ -220,6 +220,8 @@ providers:
220
220
  | **オプションプロファイル** | `providers.yaml` に名前付きプリセットを定義 → ドロップダウンで選択するだけ |
221
221
  | **複数プロセス管理** | llama.cpp と vllm を同時に起動し、ポートごとに独立管理 |
222
222
  | **ログビューア** | 各プロセスの stdout/stderr をブラウザ内でリアルタイム確認 |
223
+ | **provider 自動同期** (v2.7.4) | 起動したバックエンドを provider として自動登録(`launcher-llamacpp-8085` 等)。providers.yaml 無編集で `X-CodeRouter-Profile: launcher` からルーティング可能。メモリ内のみ・serve と同寿命 |
224
+ | **モデル名パススルー** (v2.7.4) | `model: ""` の provider は `/v1/models` が上流のロード中モデル ID(gguf 名)をそのまま返す。gguf を差し替えても config 編集不要 — 外部ベンチからモデルを識別できる |
223
225
 
224
226
  ```yaml
225
227
  # providers.yaml に追記するだけで有効になる
@@ -156,6 +156,8 @@ Browser UI at `http://localhost:8088/launcher` for starting and managing local i
156
156
  | **Option profiles** | Name your flag presets in `providers.yaml` — select from a dropdown, no CLI needed |
157
157
  | **Multi-process** | Run llama.cpp and vllm side by side on different ports |
158
158
  | **Log viewer** | stdout/stderr of each process shown live in the browser |
159
+ | **Provider auto-sync** (v2.7.4) | A started backend is auto-registered as a routable provider (`launcher-llamacpp-8085`); route to it with `X-CodeRouter-Profile: launcher` — zero providers.yaml edits. In-memory, shares the server's lifetime |
160
+ | **Model-id passthrough** (v2.7.4) | Providers with `model: ""` make `/v1/models` surface the upstream's actually-loaded model id (the GGUF name). Swap models without touching config — external benchmarks can tell runs apart |
159
161
 
160
162
  ```yaml
161
163
  # Add to providers.yaml — no code changes needed
@@ -175,6 +175,8 @@ providers:
175
175
  | **オプションプロファイル** | `providers.yaml` に名前付きプリセットを定義 → ドロップダウンで選択するだけ |
176
176
  | **複数プロセス管理** | llama.cpp と vllm を同時に起動し、ポートごとに独立管理 |
177
177
  | **ログビューア** | 各プロセスの stdout/stderr をブラウザ内でリアルタイム確認 |
178
+ | **provider 自動同期** (v2.7.4) | 起動したバックエンドを provider として自動登録(`launcher-llamacpp-8085` 等)。providers.yaml 無編集で `X-CodeRouter-Profile: launcher` からルーティング可能。メモリ内のみ・serve と同寿命 |
179
+ | **モデル名パススルー** (v2.7.4) | `model: ""` の provider は `/v1/models` が上流のロード中モデル ID(gguf 名)をそのまま返す。gguf を差し替えても config 編集不要 — 外部ベンチからモデルを識別できる |
178
180
 
179
181
  ```yaml
180
182
  # providers.yaml に追記するだけで有効になる
@@ -718,6 +718,57 @@ class FallbackChain(BaseModel):
718
718
  ),
719
719
  )
720
720
 
721
+ # --- ⑧ (empty-response): per-request empty-response fallback ------------
722
+ #
723
+ # Some local backends (observed: gemma4:26b on ``no_tool_temptation``
724
+ # prompts) return a 200 with a structurally-valid but *content-empty*
725
+ # Anthropic response — no tool_use and no non-whitespace text — for a
726
+ # fraction of requests. The drift guard's ``empty_response_rate`` is a
727
+ # windowed aggregate (default threshold 0.3, ``min_window_fill`` 6): it
728
+ # promotes/reloads a backend once the *rate* is bad, but cannot rescue a
729
+ # single blank turn in-flight. This knob adds a per-request in-flight
730
+ # fallback that re-dispatches the *same* request to the next provider in
731
+ # the chain the moment an empty response is detected.
732
+ #
733
+ # Design: "empty" is judged on *content*, not usage.output_tokens (which
734
+ # some backends report unreliably). A response is empty when its content
735
+ # list is empty, or every block is either a whitespace-only ``text`` block
736
+ # or a ``thinking`` block — i.e. nothing the client can act on. A single
737
+ # ``tool_use`` block or one non-whitespace ``text`` block makes it non-empty.
738
+ #
739
+ # * ``off`` — no detection, no fallback, no log. Backward-compatible
740
+ # default (byte-for-byte identical to pre-⑧ behavior).
741
+ # * ``warn`` — detect + emit an ``empty-response-detected`` log line
742
+ # only; the empty response is returned unchanged.
743
+ # * ``fallback`` — on an empty response, log ``empty-response-detected``
744
+ # and continue to the next provider (the empty response
745
+ # is *not* recorded as an error). If every provider in
746
+ # the chain returns empty, the last empty response is
747
+ # returned as-is (a 200 blank is a legitimate answer)
748
+ # with ``chain_exhausted=True`` on the log line. On the
749
+ # streaming path, ``fallback`` buffers events until real
750
+ # content is observed, so an empty stream can be swapped
751
+ # to the next provider without the client seeing bytes.
752
+ #
753
+ # Default ``off`` preserves complete backward compatibility; the original
754
+ # request object is never mutated, so a later provider always receives the
755
+ # untouched request.
756
+ empty_response_action: Literal["off", "warn", "fallback"] = Field(
757
+ default="off",
758
+ description=(
759
+ "⑧ (empty-response): action when a provider returns a 200 with "
760
+ "empty content (no tool_use and no non-whitespace text; a "
761
+ "thinking-only response counts as empty). ``off`` (default) does "
762
+ "nothing. ``warn`` emits an ``empty-response-detected`` log only "
763
+ "and returns the empty response. ``fallback`` re-dispatches the "
764
+ "same request to the next provider (empty is not counted as an "
765
+ "error); if the whole chain returns empty, the last empty response "
766
+ "is returned unchanged. Streaming buffers events until real "
767
+ "content appears so empty streams can be swapped provider-side "
768
+ "without the client seeing any bytes."
769
+ ),
770
+ )
771
+
721
772
  # --- S2 (shim): tool_choice capability gate + emulation -----------------
722
773
  #
723
774
  # Anthropic clients (Claude Code, SDKs) can pin the model to a specific
@@ -655,6 +655,28 @@ async def api_backends(request: Request) -> dict[str, Any]:
655
655
  return {"backends": result}
656
656
 
657
657
 
658
+ def _launcher_provider_config(backend: str, port: int) -> Any:
659
+ """Build the provider entry for a launcher-started backend.
660
+
661
+ ``model`` is left EMPTY on purpose: llama-server / vllm / mlx decide
662
+ which model is actually loaded, and the empty-model /v1/models
663
+ passthrough then surfaces the upstream's real model id to benchmark
664
+ clients — swap the GGUF, no config edit needed.
665
+
666
+ Name embeds backend + port (``launcher-llamacpp-8085``) so restarting
667
+ on the same port REPLACES the entry instead of piling up duplicates.
668
+ """
669
+ from coderouter.config.schemas import ProviderConfig
670
+
671
+ safe_backend = backend.replace(".", "")
672
+ return ProviderConfig(
673
+ name=f"launcher-{safe_backend}-{port}",
674
+ base_url=f"http://localhost:{port}/v1",
675
+ model="",
676
+ timeout_s=120.0,
677
+ )
678
+
679
+
658
680
  @router.post("/api/launcher/start")
659
681
  async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
660
682
  """Start a new backend process."""
@@ -717,7 +739,35 @@ async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
717
739
  _background_tasks.add(_task)
718
740
  _task.add_done_callback(_background_tasks.discard)
719
741
 
720
- return {"id": proc_id, "pid": p.pid, "command": cmd}
742
+ # Provider auto-sync: register the just-started backend as a routable
743
+ # provider so requests can reach it without hand-editing providers.yaml
744
+ # (in-memory only — see FallbackEngine.register_provider docstring).
745
+ # Sync failure must never fail the start itself.
746
+ provider_sync: dict[str, Any] | None = None
747
+ engine = getattr(request.app.state, "engine", None)
748
+ if engine is not None and hasattr(engine, "register_provider"):
749
+ try:
750
+ provider_sync = engine.register_provider(
751
+ _launcher_provider_config(req.backend, req.port)
752
+ )
753
+ proc.log_tail.append(
754
+ "[launcher] provider sync: "
755
+ f"{provider_sync['provider']} -> profile "
756
+ f"'{provider_sync['profile']}' (in-memory)"
757
+ )
758
+ except Exception as exc:
759
+ logger.warning(
760
+ "launcher provider sync failed",
761
+ extra={"backend": req.backend, "port": req.port, "error": str(exc)},
762
+ )
763
+ proc.log_tail.append(f"[launcher] provider sync failed: {exc}")
764
+
765
+ return {
766
+ "id": proc_id,
767
+ "pid": p.pid,
768
+ "command": cmd,
769
+ "provider_sync": provider_sync,
770
+ }
721
771
 
722
772
 
723
773
  @router.post("/api/launcher/stop/{proc_id}")
@@ -78,22 +78,99 @@ def _resolve_stream_timeout_s(engine: FallbackEngine, profile: str | None) -> fl
78
78
  return max(_STREAM_TIMEOUT_MIN_S, float(per_call) * _STREAM_TIMEOUT_MULTIPLIER)
79
79
 
80
80
 
81
+ # --- /v1/models upstream passthrough -------------------------------------
82
+ #
83
+ # Providers with an *empty* ``model`` field are passthrough providers: the
84
+ # upstream (llama-server, LM Studio, ...) decides which model is loaded and
85
+ # CodeRouter never sends a model name. For those providers the historic
86
+ # behaviour — returning the provider *name* — makes external benchmarks
87
+ # indistinguishable across loaded GGUFs (the launcher_gui setup always
88
+ # reported ``llama-cpp-local`` regardless of the selected model). We now ask
89
+ # the upstream's own ``/models`` endpoint for its real model id(s) and
90
+ # surface those instead.
91
+ #
92
+ # Guards: only for ``kind == "openai_compat"`` with ``model == ""``; a short
93
+ # per-upstream TTL cache keeps repeated SDK probes cheap; ANY failure (refused
94
+ # connection, timeout, unexpected shape) falls back to the historic
95
+ # provider-name entry, so this is strictly additive. Providers with a
96
+ # configured model keep the exact previous behaviour.
97
+
98
+ _UPSTREAM_MODELS_TTL_S = 30.0
99
+ _UPSTREAM_MODELS_TIMEOUT_S = 2.0
100
+ # base_url -> (expires_monotonic, ids)
101
+ _upstream_models_cache: dict[str, tuple[float, list[str]]] = {}
102
+
103
+
104
+ async def _fetch_upstream_model_ids(base_url: str) -> list[str]:
105
+ """GET {base_url}/models and return the upstream model ids.
106
+
107
+ Raises on any transport / shape problem — the caller treats every
108
+ exception as "fall back to the provider-name entry".
109
+ """
110
+ import httpx # local import keeps module import time flat
111
+
112
+ url = base_url.rstrip("/") + "/models"
113
+ async with httpx.AsyncClient(timeout=_UPSTREAM_MODELS_TIMEOUT_S) as client:
114
+ resp = await client.get(url)
115
+ resp.raise_for_status()
116
+ payload = resp.json()
117
+ ids = [
118
+ str(item["id"])
119
+ for item in payload.get("data", [])
120
+ if isinstance(item, dict) and item.get("id")
121
+ ]
122
+ if not ids:
123
+ raise ValueError(f"upstream {url} returned no model ids")
124
+ return ids
125
+
126
+
127
+ async def _upstream_model_ids_cached(base_url: str) -> list[str]:
128
+ """TTL-cached wrapper around :func:`_fetch_upstream_model_ids`."""
129
+ now = time.monotonic()
130
+ hit = _upstream_models_cache.get(base_url)
131
+ if hit is not None and hit[0] > now:
132
+ return hit[1]
133
+ ids = await _fetch_upstream_model_ids(base_url)
134
+ _upstream_models_cache[base_url] = (now + _UPSTREAM_MODELS_TTL_S, ids)
135
+ return ids
136
+
137
+
81
138
  @router.get("/models")
82
139
  async def list_models(request: Request) -> dict[str, object]:
83
- """Minimal /v1/models so OpenAI SDKs that probe it don't choke."""
140
+ """/v1/models: provider names, with upstream passthrough for empty-model
141
+ providers (see the passthrough note above)."""
84
142
  config = request.app.state.config
85
- return {
86
- "object": "list",
87
- "data": [
143
+ created = int(time.time())
144
+ data: list[dict[str, object]] = []
145
+ for p in config.providers:
146
+ if p.model == "" and p.kind == "openai_compat":
147
+ try:
148
+ ids = await _upstream_model_ids_cached(str(p.base_url))
149
+ except Exception: # any failure means "no passthrough"
150
+ logger.debug(
151
+ "models passthrough failed; falling back to provider name",
152
+ extra={"provider": p.name},
153
+ )
154
+ else:
155
+ data.extend(
156
+ {
157
+ "id": model_id,
158
+ "object": "model",
159
+ "created": created,
160
+ "owned_by": f"coderouter/{p.name}",
161
+ }
162
+ for model_id in ids
163
+ )
164
+ continue
165
+ data.append(
88
166
  {
89
167
  "id": p.name,
90
168
  "object": "model",
91
- "created": int(time.time()),
169
+ "created": created,
92
170
  "owned_by": "coderouter",
93
171
  }
94
- for p in config.providers
95
- ],
96
- }
172
+ )
173
+ return {"object": "list", "data": data}
97
174
 
98
175
 
99
176
  @router.post("/chat/completions", response_model=None)
@@ -1372,6 +1372,46 @@ def log_partial_stitch_surfaced(
1372
1372
  )
1373
1373
 
1374
1374
 
1375
+ # ---------------------------------------------------------------------------
1376
+ # ⑧ (empty-response): per-request empty-response fallback log shape
1377
+ #
1378
+ # Fired when a provider returns a 200 with content-empty output (no
1379
+ # tool_use and no non-whitespace text). The single event lane carries the
1380
+ # ``action`` (warn|fallback) that triggered it, whether the empty response
1381
+ # came from the streaming or non-streaming path, and ``chain_exhausted``
1382
+ # — True only when every provider in the chain returned empty and the last
1383
+ # empty response is being returned to the client as-is.
1384
+ # ---------------------------------------------------------------------------
1385
+
1386
+
1387
+ def log_empty_response_detected(
1388
+ logger: logging.Logger,
1389
+ provider: str,
1390
+ *,
1391
+ action: str,
1392
+ stream: bool,
1393
+ chain_exhausted: bool = False,
1394
+ ) -> None:
1395
+ """Emit an ``empty-response-detected`` warn line.
1396
+
1397
+ Fired when ``empty_response_action`` is ``warn`` or ``fallback`` and a
1398
+ provider returns a structurally-valid but content-empty response. The
1399
+ ``action`` field records which knob value produced the line; ``stream``
1400
+ distinguishes the SSE path from the non-streaming path; ``chain_exhausted``
1401
+ is True only when the whole chain returned empty and the last empty
1402
+ response is being returned unchanged.
1403
+ """
1404
+ logger.warning(
1405
+ "empty-response-detected",
1406
+ extra={
1407
+ "provider": provider,
1408
+ "action": action,
1409
+ "stream": stream,
1410
+ "chain_exhausted": chain_exhausted,
1411
+ },
1412
+ )
1413
+
1414
+
1375
1415
  # ---------------------------------------------------------------------------
1376
1416
  # v2.0-I: Continuous probe log shapes
1377
1417
  #
@@ -52,6 +52,7 @@ Event inventory (dispatch table in :meth:`MetricsCollector._dispatch`)
52
52
  ``drift-promoted`` (v2.0-G) → ``drift_promoted_total``
53
53
  ``drift-reload-attempted`` → ``drift_reload_total`` / success
54
54
  ``partial-stitch-surfaced`` → ``partial_stitch_surfaced_total`` (v2.0-H)
55
+ ``empty-response-detected`` → ``empty_responses_total`` + per-provider (⑧)
55
56
  ``probe-completed`` (v2.0-I) → ``probe_total`` / ``probe_success`` / ``probe_failure``
56
57
  + per-provider latency gauge
57
58
  ``probe-round-completed`` → ``probe_rounds_total`` (v2.0-I)
@@ -109,6 +110,7 @@ _KNOWN_EVENTS: Final[frozenset[str]] = frozenset(
109
110
  "drift-promoted",
110
111
  "drift-reload-attempted",
111
112
  "partial-stitch-surfaced",
113
+ "empty-response-detected",
112
114
  "probe-completed",
113
115
  "probe-round-completed",
114
116
  "probe-capabilities-drift",
@@ -272,6 +274,13 @@ class MetricsCollector(logging.Handler):
272
274
  # client instead of a generic error event.
273
275
  self._partial_stitch_surfaced_total: int = 0
274
276
 
277
+ # ⑧ (empty-response): count of per-request empty-response detections
278
+ # (both ``warn`` and ``fallback`` actions). Aggregate + per-provider
279
+ # so a dashboard can spot which backend blanks out. Counts every
280
+ # detection event, including the terminal chain-exhausted one.
281
+ self._empty_responses_total: int = 0
282
+ self._empty_responses_by_provider: Counter[str] = Counter()
283
+
275
284
  # v2.0-I: continuous probe counters. Per-provider probe attempts
276
285
  # and outcomes, plus a round counter for the dashboard's
277
286
  # "probes/min" panel.
@@ -329,6 +338,7 @@ class MetricsCollector(logging.Handler):
329
338
  "drift-promoted": self._on_drift_promoted,
330
339
  "drift-reload-attempted": self._on_drift_reload_attempted,
331
340
  "partial-stitch-surfaced": self._on_partial_stitch_surfaced,
341
+ "empty-response-detected": self._on_empty_response_detected,
332
342
  "probe-completed": self._on_probe_completed,
333
343
  "probe-round-completed": self._on_probe_round_completed,
334
344
  "probe-capabilities-drift": self._on_probe_capabilities_drift,
@@ -670,6 +680,17 @@ class MetricsCollector(logging.Handler):
670
680
  self._partial_stitch_surfaced_total += 1
671
681
  self._push_recent("partial-stitch-surfaced", extras, record)
672
682
 
683
+ def _on_empty_response_detected(
684
+ self, extras: dict[str, Any], record: logging.LogRecord
685
+ ) -> None:
686
+ # ⑧ (empty-response): a content-empty 200 was detected under a
687
+ # ``warn`` / ``fallback`` action.
688
+ self._empty_responses_total += 1
689
+ provider = _str(extras.get("provider"))
690
+ if provider:
691
+ self._empty_responses_by_provider[provider] += 1
692
+ self._push_recent("empty-response-detected", extras, record)
693
+
673
694
  def _on_probe_completed(
674
695
  self, extras: dict[str, Any], record: logging.LogRecord
675
696
  ) -> None:
@@ -888,6 +909,11 @@ class MetricsCollector(logging.Handler):
888
909
  "drift_reload_success_total": self._drift_reload_success_total,
889
910
  # v2.0-H (L6): partial stitch surfaced.
890
911
  "partial_stitch_surfaced_total": self._partial_stitch_surfaced_total,
912
+ # ⑧ (empty-response): per-request empty-response fallback.
913
+ "empty_responses_total": self._empty_responses_total,
914
+ "empty_responses_by_provider": dict(
915
+ self._empty_responses_by_provider
916
+ ),
891
917
  # v2.0-I: continuous probe counters.
892
918
  "probe_total": dict(self._probe_total),
893
919
  "probe_success": dict(self._probe_success),
@@ -1042,6 +1068,9 @@ class MetricsCollector(logging.Handler):
1042
1068
  self._tokens_saved_by_mechanism.clear()
1043
1069
  # v2.0-H (L6)
1044
1070
  self._partial_stitch_surfaced_total = 0
1071
+ # ⑧ (empty-response)
1072
+ self._empty_responses_total = 0
1073
+ self._empty_responses_by_provider.clear()
1045
1074
  # v2.0-I
1046
1075
  self._probe_total.clear()
1047
1076
  self._probe_success.clear()