coderouter-cli 2.7.3__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.3 → coderouter_cli-2.7.4}/CHANGELOG.md +43 -0
  2. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/PKG-INFO +3 -1
  3. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/README.en.md +2 -0
  4. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/README.md +2 -0
  5. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/ingress/launcher_routes.py +51 -1
  6. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/ingress/openai_routes.py +85 -8
  7. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/routing/fallback.py +74 -0
  8. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/backends/launcher.md +25 -0
  9. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/pyproject.toml +1 -1
  10. coderouter_cli-2.7.4/tests/test_launcher_provider_sync.py +124 -0
  11. coderouter_cli-2.7.4/tests/test_models_passthrough.py +144 -0
  12. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/.gitignore +0 -0
  13. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/LICENSE +0 -0
  14. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/__init__.py +0 -0
  15. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/__main__.py +0 -0
  16. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/adapters/__init__.py +0 -0
  17. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/adapters/anthropic_native.py +0 -0
  18. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/adapters/base.py +0 -0
  19. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/adapters/openai_compat.py +0 -0
  20. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/adapters/registry.py +0 -0
  21. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/cli.py +0 -0
  22. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/cli_stats.py +0 -0
  23. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/config/__init__.py +0 -0
  24. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/config/capability_registry.py +0 -0
  25. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/config/env_file.py +0 -0
  26. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/config/loader.py +0 -0
  27. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/config/schemas.py +0 -0
  28. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/cost.py +0 -0
  29. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/data/__init__.py +0 -0
  30. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/data/model-capabilities.yaml +0 -0
  31. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/doctor.py +0 -0
  32. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/doctor_apply.py +0 -0
  33. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/env_security.py +0 -0
  34. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/errors.py +0 -0
  35. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/gguf_introspect.py +0 -0
  36. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/guards/__init__.py +0 -0
  37. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/guards/_fingerprint.py +0 -0
  38. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/guards/backend_health.py +0 -0
  39. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/guards/context_budget.py +0 -0
  40. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/guards/continuous_probe.py +0 -0
  41. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/guards/drift_actions.py +0 -0
  42. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/guards/drift_detection.py +0 -0
  43. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/guards/memory_budget.py +0 -0
  44. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/guards/memory_pressure.py +0 -0
  45. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/guards/self_healing.py +0 -0
  46. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/guards/tool_loop.py +0 -0
  47. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/hardware.py +0 -0
  48. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/ingress/__init__.py +0 -0
  49. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/ingress/anthropic_routes.py +0 -0
  50. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/ingress/app.py +0 -0
  51. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/ingress/dashboard_routes.py +0 -0
  52. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/ingress/metrics_routes.py +0 -0
  53. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/language_tax.py +0 -0
  54. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/logging.py +0 -0
  55. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/metrics/__init__.py +0 -0
  56. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/metrics/collector.py +0 -0
  57. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/metrics/prometheus.py +0 -0
  58. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/output_filters.py +0 -0
  59. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/plugins/__init__.py +0 -0
  60. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/plugins/base.py +0 -0
  61. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/plugins/loader.py +0 -0
  62. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/plugins/registry.py +0 -0
  63. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/routing/__init__.py +0 -0
  64. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/routing/adaptive.py +0 -0
  65. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/routing/auto_router.py +0 -0
  66. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/routing/budget.py +0 -0
  67. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/routing/capability.py +0 -0
  68. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/state/__init__.py +0 -0
  69. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/state/audit_log.py +0 -0
  70. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/state/replay.py +0 -0
  71. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/state/request_log.py +0 -0
  72. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/state/store.py +0 -0
  73. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/state/suggest_rules.py +0 -0
  74. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/token_estimation.py +0 -0
  75. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/token_estimation_accurate.py +0 -0
  76. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/translation/__init__.py +0 -0
  77. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/translation/anthropic.py +0 -0
  78. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/translation/convert.py +0 -0
  79. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/coderouter/translation/tool_repair.py +0 -0
  80. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/README.md +0 -0
  81. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/assets/dashboard-demo.png +0 -0
  82. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
  83. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/backends/gguf_dl.md +0 -0
  84. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/backends/hf-ollama-models.md +0 -0
  85. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/backends/install-backends.en.md +0 -0
  86. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/backends/install-backends.md +0 -0
  87. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/backends/launcher-quickstart.md +0 -0
  88. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/backends/llamacpp-direct.en.md +0 -0
  89. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/backends/llamacpp-direct.md +0 -0
  90. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/backends/lmstudio-direct.en.md +0 -0
  91. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/backends/lmstudio-direct.md +0 -0
  92. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/backends/verify-ollama-0.23.1.md +0 -0
  93. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/concepts/architecture.md +0 -0
  94. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/concepts/context-budget.md +0 -0
  95. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/concepts/continuous-probing.md +0 -0
  96. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/concepts/drift-detection.md +0 -0
  97. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/concepts/partial-stitch.md +0 -0
  98. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  99. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/designs/v1.6-auto-router-verification.md +0 -0
  100. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/designs/v1.6-auto-router.md +0 -0
  101. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/guides/free-tier-guide.en.md +0 -0
  102. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/guides/free-tier-guide.md +0 -0
  103. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/guides/language-tax.en.md +0 -0
  104. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/guides/language-tax.md +0 -0
  105. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/guides/security.en.md +0 -0
  106. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/guides/security.md +0 -0
  107. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/guides/troubleshooting.en.md +0 -0
  108. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/guides/troubleshooting.md +0 -0
  109. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/guides/usage-guide.en.md +0 -0
  110. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/guides/usage-guide.md +0 -0
  111. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/low-memory-integration.md +0 -0
  112. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/openrouter-roster/CHANGES.md +0 -0
  113. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/openrouter-roster/README.md +0 -0
  114. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/openrouter-roster/latest.json +0 -0
  115. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/retrospectives/v0.4.md +0 -0
  116. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/retrospectives/v0.5-verify.md +0 -0
  117. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/retrospectives/v0.5.md +0 -0
  118. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/retrospectives/v0.6.md +0 -0
  119. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/retrospectives/v0.7.md +0 -0
  120. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/retrospectives/v1.0-verify.md +0 -0
  121. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/retrospectives/v1.0.md +0 -0
  122. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/start/quickstart.en.md +0 -0
  123. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/start/quickstart.md +0 -0
  124. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/start/when-do-i-need-coderouter.en.md +0 -0
  125. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/docs/start/when-do-i-need-coderouter.md +0 -0
  126. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/examples/.env.example +0 -0
  127. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/examples/README.md +0 -0
  128. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/examples/providers-multiagent.yaml +0 -0
  129. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/examples/providers.context-budget-test.yaml +0 -0
  130. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/examples/providers.llamacpp-vllm.yaml +0 -0
  131. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/examples/providers.nvidia-nim.yaml +0 -0
  132. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/examples/providers.ollama-auto-custom.yaml +0 -0
  133. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/examples/providers.ollama-auto.yaml +0 -0
  134. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/examples/providers.ollama-free-chain.yaml +0 -0
  135. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/examples/providers.raspberrypi.yaml +0 -0
  136. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/examples/providers.yaml +0 -0
  137. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/scripts/demo_traffic.sh +0 -0
  138. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/scripts/openrouter_roster_diff.py +0 -0
  139. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/scripts/smoke_v2_2.sh +0 -0
  140. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/scripts/verify-providers.yaml +0 -0
  141. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/scripts/verify_ollama_0_23.py +0 -0
  142. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/scripts/verify_v0_5.sh +0 -0
  143. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/scripts/verify_v1_0.sh +0 -0
  144. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/__init__.py +0 -0
  145. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/conftest.py +0 -0
  146. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_adapter_anthropic.py +0 -0
  147. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_audit_log.py +0 -0
  148. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_auto_router.py +0 -0
  149. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_auto_router_cjk.py +0 -0
  150. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_backend_health.py +0 -0
  151. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_budget.py +0 -0
  152. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_capability.py +0 -0
  153. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_capability_degraded_payload.py +0 -0
  154. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_capability_registry.py +0 -0
  155. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_capability_registry_cache_control.py +0 -0
  156. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_capability_tool_choice.py +0 -0
  157. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_claude_code_suitability.py +0 -0
  158. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_cli.py +0 -0
  159. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_cli_stats.py +0 -0
  160. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_config.py +0 -0
  161. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_config_shim_actions.py +0 -0
  162. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_context_budget.py +0 -0
  163. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_continuous_probe.py +0 -0
  164. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_dashboard_endpoint.py +0 -0
  165. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_doctor.py +0 -0
  166. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_doctor_apply.py +0 -0
  167. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_doctor_cache_probe.py +0 -0
  168. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_drift_actions.py +0 -0
  169. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_drift_detection.py +0 -0
  170. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_drift_detection_integration.py +0 -0
  171. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_env_file.py +0 -0
  172. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_env_security.py +0 -0
  173. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_errors.py +0 -0
  174. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_examples_yaml.py +0 -0
  175. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fallback.py +0 -0
  176. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fallback_anthropic.py +0 -0
  177. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fallback_cache_control.py +0 -0
  178. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fallback_cache_observed.py +0 -0
  179. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fallback_cache_strip.py +0 -0
  180. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fallback_empty_response.py +0 -0
  181. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fallback_misconfig_warn.py +0 -0
  182. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fallback_paid_gate.py +0 -0
  183. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fallback_thinking.py +0 -0
  184. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fallback_tool_choice_emulate.py +0 -0
  185. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fix_h1_prometheus_drift.py +0 -0
  186. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fix_h2_h6_translation.py +0 -0
  187. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fix_h3_client_reuse.py +0 -0
  188. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fix_h4_h5_guards.py +0 -0
  189. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fix_h7_probe_restore.py +0 -0
  190. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fix_h8_launcher_auth.py +0 -0
  191. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fix_mA_routing.py +0 -0
  192. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fix_mB_adapters.py +0 -0
  193. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fix_mC_convert.py +0 -0
  194. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fix_mD_logging_metrics.py +0 -0
  195. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fix_mE_config_validation.py +0 -0
  196. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_fix_mF_ingress.py +0 -0
  197. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_gguf_introspect.py +0 -0
  198. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_guards_tool_loop.py +0 -0
  199. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_hardware.py +0 -0
  200. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_ingress_anthropic.py +0 -0
  201. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_ingress_count_tokens.py +0 -0
  202. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_ingress_profile.py +0 -0
  203. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_language_tax.py +0 -0
  204. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_language_tax_integration.py +0 -0
  205. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_memory_budget.py +0 -0
  206. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_memory_pressure.py +0 -0
  207. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_metrics_cache.py +0 -0
  208. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_metrics_collector.py +0 -0
  209. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_metrics_cost.py +0 -0
  210. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_metrics_endpoint.py +0 -0
  211. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_metrics_jsonl.py +0 -0
  212. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_metrics_prometheus.py +0 -0
  213. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_metrics_prometheus_cache.py +0 -0
  214. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_openai_compat.py +0 -0
  215. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_openrouter_roster_diff.py +0 -0
  216. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_output_filters.py +0 -0
  217. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_output_filters_adapters.py +0 -0
  218. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_partial_stitch.py +0 -0
  219. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_plugins_integration.py +0 -0
  220. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_plugins_loader.py +0 -0
  221. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_plugins_registry.py +0 -0
  222. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_reasoning_strip.py +0 -0
  223. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_repair_byte_fallback.py +0 -0
  224. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_request_log.py +0 -0
  225. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_role_normalization.py +0 -0
  226. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_routing_adaptive.py +0 -0
  227. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_self_healing.py +0 -0
  228. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_setup_sh.py +0 -0
  229. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_state_store.py +0 -0
  230. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_token_estimation.py +0 -0
  231. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_token_estimation_accurate.py +0 -0
  232. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_tokens_saved_metric.py +0 -0
  233. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_tool_repair.py +0 -0
  234. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_tool_repair_lenient.py +0 -0
  235. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_tool_repair_r4.py +0 -0
  236. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_toolrepair_bench.py +0 -0
  237. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_translation_anthropic.py +0 -0
  238. {coderouter_cli-2.7.3 → coderouter_cli-2.7.4}/tests/test_translation_reverse.py +0 -0
@@ -6,6 +6,49 @@ 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
+
9
52
  ## [v2.7.3] — 2026-07-05 (Per-request empty-response fallback + L3 bench evidence)
10
53
 
11
54
  Two PRs: **PR #45** (`empty_response_action` — the fallback leg of the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.7.3
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 に追記するだけで有効になる
@@ -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)
@@ -1173,6 +1173,80 @@ class FallbackEngine:
1173
1173
  # v2.0-K: persistent state store (None = in-memory only).
1174
1174
  self._state_store: StateStore | None = None
1175
1175
 
1176
+ def register_provider(
1177
+ self, provider: ProviderConfig, profile_name: str = "launcher"
1178
+ ) -> dict[str, Any]:
1179
+ """Register (or update) a provider at runtime — launcher auto-sync.
1180
+
1181
+ Motivation: the embedded launcher can start a llama.cpp / vllm /
1182
+ mlx backend on an arbitrary port, but routing is driven entirely
1183
+ by providers.yaml — historically the operator had to hand-edit the
1184
+ config or the new backend was simply unreachable (observed in the
1185
+ wild as "launcher started on 8085, providers.yaml pointed at
1186
+ 8080"). This method closes that gap in-process:
1187
+
1188
+ - the provider entry is appended to ``config.providers`` (or, when
1189
+ a provider with the same *name* already exists, its config entry
1190
+ and cached adapter are REPLACED — the launcher restarting a
1191
+ backend on the same port lands here),
1192
+ - an adapter is built and cached alongside the static ones,
1193
+ - the ``profile_name`` chain is created on demand, and the
1194
+ provider is moved to the FRONT of that chain (most recently
1195
+ launched backend wins the launcher profile).
1196
+
1197
+ Deliberately **in-memory only** — no providers.yaml rewrite. A
1198
+ YAML round-trip would destroy operator comments (and a
1199
+ comment-preserving writer means a new dependency), and the
1200
+ launcher's process registry is itself in-memory by design: both
1201
+ the process and its provider entry share the lifetime of this
1202
+ server process. ``default_profile`` is never touched — routing to
1203
+ the launcher profile stays an explicit opt-in
1204
+ (``X-CodeRouter-Profile: launcher`` or body ``profile``).
1205
+
1206
+ Returns a small summary dict for the launcher API response.
1207
+ """
1208
+ from coderouter.config.schemas import FallbackChain
1209
+
1210
+ replaced = False
1211
+ for i, existing in enumerate(self.config.providers):
1212
+ if existing.name == provider.name:
1213
+ self.config.providers[i] = provider
1214
+ replaced = True
1215
+ break
1216
+ if not replaced:
1217
+ self.config.providers.append(provider)
1218
+ self._adapters[provider.name] = build_adapter(provider)
1219
+
1220
+ try:
1221
+ chain = self.config.profile_by_name(profile_name)
1222
+ except KeyError:
1223
+ chain = FallbackChain(name=profile_name, providers=[provider.name])
1224
+ self.config.profiles.append(chain)
1225
+ else:
1226
+ if provider.name in chain.providers:
1227
+ chain.providers.remove(provider.name)
1228
+ chain.providers.insert(0, provider.name)
1229
+
1230
+ logger.info(
1231
+ "launcher provider sync: registered %s -> %s (profile %r)",
1232
+ provider.name,
1233
+ provider.base_url,
1234
+ profile_name,
1235
+ extra={
1236
+ "provider": provider.name,
1237
+ "base_url": str(provider.base_url),
1238
+ "profile": profile_name,
1239
+ "replaced": replaced,
1240
+ },
1241
+ )
1242
+ return {
1243
+ "provider": provider.name,
1244
+ "base_url": str(provider.base_url),
1245
+ "profile": profile_name,
1246
+ "replaced": replaced,
1247
+ "persisted": False, # in-memory only, by design (see docstring)
1248
+ }
1249
+
1176
1250
  @property
1177
1251
  def plugins(self) -> PluginRegistry:
1178
1252
  """Return the plugin registry, lazily building an empty one if absent.
@@ -110,6 +110,31 @@ Launcher の画面は「MODELS パネル」「LAUNCH フォーム」「PROCESSES
110
110
 
111
111
  `▶ 起動` でプロセスが起動し、PROCESSES テーブルに表示されます。バイナリが見つからない場合は**起動ボタンが自動的に無効化**され、理由が表示されます。「追加オプション」欄の横の **⚙ 推奨値** ボタンについては [メモリ推奨](#メモリ推奨) を参照してください。
112
112
 
113
+ ### provider 自動同期(v2.7.4、Web版のみ)
114
+
115
+ Web 版で backend を起動すると、その backend が **provider として自動登録**されます(providers.yaml の編集不要)。
116
+
117
+ - provider 名は `launcher-<backend>-<port>`(例: `launcher-llamacpp-8085`)。同名で再起動するとエントリは**置き換え**られ、重複しません
118
+ - 登録先は `launcher` プロファイル(無ければ自動作成)。**最後に起動した backend が先頭**に来ます
119
+ - ルーティングは明示オプトイン: `X-CodeRouter-Profile: launcher` ヘッダ、または body の `"profile": "launcher"`。**`default_profile` は変更されません**
120
+ - 登録は**メモリ内のみ**です(providers.yaml には書き込みません — 手書きコメントを壊さないため)。serve を再起動すると消えますが、Launcher のプロセス自体も同寿命なので整合します。恒久化したい場合は providers.yaml に手で転記してください
121
+ - provider は `model: ""` で登録されるため、`/v1/models` は**上流がロード中の実モデル ID(gguf 名)**を返します(モデル名パススルー、同 v2.7.4)。gguf を差し替えても config 編集は不要で、外部ベンチマークからモデルを識別できます(30 秒 TTL キャッシュあり)
122
+
123
+ 動作確認:
124
+
125
+ ```bash
126
+ # 起動後
127
+ curl http://localhost:8088/v1/models
128
+ # → "id": "<ロード中のgguf名>", "owned_by": "coderouter/launcher-llamacpp-8085"
129
+
130
+ curl http://localhost:8088/v1/chat/completions \
131
+ -H 'Content-Type: application/json' -H 'X-CodeRouter-Profile: launcher' \
132
+ -d '{"model":"x","messages":[{"role":"user","content":"say hi"}]}'
133
+ # → coderouter_provider が launcher-llamacpp-<port> なら疎通OK
134
+ ```
135
+
136
+ デスクトップ GUI 版(launcher_gui.py)は別プロセスで動くため自動同期の対象外です。従来どおり providers.yaml のエントリ(初回自動生成される `llama-cpp-local` など)の `base_url` を、起動ポートに合わせてください。
137
+
113
138
  ### PROCESSES テーブル
114
139
 
115
140
  起動した backend プロセスの一覧です。NAME / BACKEND(llama.cpp / vllm / mlx)/ MODEL / PORT / PID / STATUS(`starting` / `running` / `stopped` / `error` を色分け)を表示し、プロセスを選んで **停止**(SIGTERM)・**削除**(レジストリから除去)・**ログ表示**ができます。
@@ -11,7 +11,7 @@
11
11
  # in plan.md §11.B; once granted, this name will become an alias and
12
12
  # `coderouter` will become the canonical distribution name.
13
13
  name = "coderouter-cli"
14
- version = "2.7.3"
14
+ version = "2.7.4"
15
15
  description = "Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible."
16
16
  readme = "README.md"
17
17
  requires-python = ">=3.12"
@@ -0,0 +1,124 @@
1
+ """Tests for launcher provider auto-sync (FallbackEngine.register_provider).
2
+
3
+ The embedded launcher starts backends on arbitrary ports, but routing is
4
+ config-driven — before this feature the operator had to hand-edit
5
+ providers.yaml (the observed failure: launcher on 8085, config pointing at
6
+ 8080). register_provider closes the gap in-memory: provider entry + adapter
7
+ + a "launcher" profile whose head is always the most recently started
8
+ backend. default_profile must never be touched.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import pytest
14
+
15
+ from coderouter.config.schemas import CodeRouterConfig, FallbackChain, ProviderConfig
16
+ from coderouter.ingress.launcher_routes import _launcher_provider_config
17
+ from coderouter.routing.fallback import FallbackEngine
18
+
19
+
20
+ def _base_config() -> CodeRouterConfig:
21
+ return CodeRouterConfig(
22
+ allow_paid=False,
23
+ default_profile="default",
24
+ providers=[
25
+ ProviderConfig(
26
+ name="static-local",
27
+ base_url="http://localhost:11434/v1",
28
+ model="qwen2.5-coder:7b",
29
+ ),
30
+ ],
31
+ profiles=[FallbackChain(name="default", providers=["static-local"])],
32
+ )
33
+
34
+
35
+ def _engine() -> FallbackEngine:
36
+ return FallbackEngine(_base_config())
37
+
38
+
39
+ def test_register_new_provider_adds_config_adapter_and_profile() -> None:
40
+ engine = _engine()
41
+ summary = engine.register_provider(
42
+ _launcher_provider_config("llama.cpp", 8085)
43
+ )
44
+
45
+ names = [p.name for p in engine.config.providers]
46
+ assert "launcher-llamacpp-8085" in names
47
+ assert "launcher-llamacpp-8085" in engine._adapters
48
+ chain = engine.config.profile_by_name("launcher")
49
+ assert chain.providers == ["launcher-llamacpp-8085"]
50
+ assert summary["replaced"] is False
51
+ assert summary["persisted"] is False
52
+
53
+
54
+ def test_reregister_same_name_replaces_instead_of_duplicating() -> None:
55
+ engine = _engine()
56
+ engine.register_provider(_launcher_provider_config("llama.cpp", 8085))
57
+ summary = engine.register_provider(
58
+ _launcher_provider_config("llama.cpp", 8085)
59
+ )
60
+
61
+ names = [p.name for p in engine.config.providers]
62
+ assert names.count("launcher-llamacpp-8085") == 1
63
+ chain = engine.config.profile_by_name("launcher")
64
+ assert chain.providers.count("launcher-llamacpp-8085") == 1
65
+ assert summary["replaced"] is True
66
+
67
+
68
+ def test_most_recent_backend_moves_to_front_of_launcher_profile() -> None:
69
+ engine = _engine()
70
+ engine.register_provider(_launcher_provider_config("llama.cpp", 8085))
71
+ engine.register_provider(_launcher_provider_config("vllm", 8081))
72
+
73
+ chain = engine.config.profile_by_name("launcher")
74
+ assert chain.providers[0] == "launcher-vllm-8081"
75
+ assert chain.providers == ["launcher-vllm-8081", "launcher-llamacpp-8085"]
76
+
77
+
78
+ def test_default_profile_is_never_touched() -> None:
79
+ engine = _engine()
80
+ engine.register_provider(_launcher_provider_config("llama.cpp", 8085))
81
+
82
+ assert engine.config.default_profile == "default"
83
+ default_chain = engine.config.profile_by_name("default")
84
+ assert default_chain.providers == ["static-local"]
85
+
86
+
87
+ def test_provider_config_shape_enables_models_passthrough() -> None:
88
+ pconf = _launcher_provider_config("llama.cpp", 8085)
89
+ # Empty model == passthrough provider: /v1/models will surface the
90
+ # upstream's loaded model id, and outbound requests omit the name.
91
+ assert pconf.model == ""
92
+ assert pconf.kind == "openai_compat"
93
+ assert str(pconf.base_url) == "http://localhost:8085/v1"
94
+
95
+
96
+ def test_static_providers_and_adapters_survive_sync() -> None:
97
+ engine = _engine()
98
+ engine.register_provider(_launcher_provider_config("llama.cpp", 8085))
99
+
100
+ assert "static-local" in engine._adapters
101
+ assert any(p.name == "static-local" for p in engine.config.providers)
102
+
103
+
104
+ def test_custom_profile_name_is_respected() -> None:
105
+ engine = _engine()
106
+ summary = engine.register_provider(
107
+ _launcher_provider_config("mlx", 9000), profile_name="bench"
108
+ )
109
+ assert summary["profile"] == "bench"
110
+ assert engine.config.profile_by_name("bench").providers == [
111
+ "launcher-mlx-9000"
112
+ ]
113
+
114
+
115
+ @pytest.mark.parametrize(
116
+ ("backend", "port", "expected"),
117
+ [
118
+ ("llama.cpp", 8085, "launcher-llamacpp-8085"),
119
+ ("vllm", 8081, "launcher-vllm-8081"),
120
+ ("mlx", 9000, "launcher-mlx-9000"),
121
+ ],
122
+ )
123
+ def test_provider_naming(backend: str, port: int, expected: str) -> None:
124
+ assert _launcher_provider_config(backend, port).name == expected
@@ -0,0 +1,144 @@
1
+ """Tests for /v1/models upstream passthrough (empty-model providers).
2
+
3
+ A provider whose ``model`` field is empty (the launcher_gui llama-server
4
+ setup) is a passthrough provider: the upstream decides which model is
5
+ loaded. /v1/models should surface the upstream's real model id(s) for such
6
+ providers so external benchmarks can tell loaded GGUFs apart, while keeping
7
+ the historic provider-name entries for everything else — and falling back
8
+ to the provider name whenever the upstream cannot be reached.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import pytest
14
+ from fastapi.testclient import TestClient
15
+
16
+ from coderouter.config.schemas import CodeRouterConfig, FallbackChain, ProviderConfig
17
+ from coderouter.ingress import openai_routes
18
+ from coderouter.ingress.app import create_app
19
+
20
+
21
+ def _config(*providers: ProviderConfig) -> CodeRouterConfig:
22
+ return CodeRouterConfig(
23
+ allow_paid=False,
24
+ default_profile="default",
25
+ providers=list(providers),
26
+ profiles=[FallbackChain(name="default", providers=[providers[0].name])],
27
+ )
28
+
29
+
30
+ def _client(
31
+ config: CodeRouterConfig, monkeypatch: pytest.MonkeyPatch
32
+ ) -> TestClient:
33
+ monkeypatch.setattr(
34
+ "coderouter.ingress.app.load_config", lambda path=None: config
35
+ )
36
+ app = create_app()
37
+ app.state.config = config
38
+ return TestClient(app)
39
+
40
+
41
+ @pytest.fixture(autouse=True)
42
+ def _clear_cache() -> None:
43
+ openai_routes._upstream_models_cache.clear()
44
+
45
+
46
+ def _passthrough_provider(name: str = "llama-cpp-local") -> ProviderConfig:
47
+ return ProviderConfig(
48
+ name=name,
49
+ base_url="http://localhost:8080/v1",
50
+ model="", # passthrough — llama-server decides the model
51
+ )
52
+
53
+
54
+ def _named_provider(name: str = "ollama-qwen") -> ProviderConfig:
55
+ return ProviderConfig(
56
+ name=name,
57
+ base_url="http://localhost:11434/v1",
58
+ model="qwen2.5-coder:7b",
59
+ )
60
+
61
+
62
+ def test_empty_model_provider_surfaces_upstream_id(
63
+ monkeypatch: pytest.MonkeyPatch,
64
+ ) -> None:
65
+ async def fake_fetch(base_url: str) -> list[str]:
66
+ assert base_url == "http://localhost:8080/v1"
67
+ return ["qwen2.5-coder-7b-q4_k_m.gguf"]
68
+
69
+ monkeypatch.setattr(openai_routes, "_fetch_upstream_model_ids", fake_fetch)
70
+ client = _client(_config(_passthrough_provider()), monkeypatch)
71
+
72
+ body = client.get("/v1/models").json()
73
+ ids = [m["id"] for m in body["data"]]
74
+ assert ids == ["qwen2.5-coder-7b-q4_k_m.gguf"]
75
+ assert body["data"][0]["owned_by"] == "coderouter/llama-cpp-local"
76
+
77
+
78
+ def test_named_model_provider_keeps_historic_shape(
79
+ monkeypatch: pytest.MonkeyPatch,
80
+ ) -> None:
81
+ async def fail_fetch(base_url: str) -> list[str]: # pragma: no cover
82
+ raise AssertionError("passthrough must not fire for named models")
83
+
84
+ monkeypatch.setattr(openai_routes, "_fetch_upstream_model_ids", fail_fetch)
85
+ client = _client(_config(_named_provider()), monkeypatch)
86
+
87
+ body = client.get("/v1/models").json()
88
+ assert [m["id"] for m in body["data"]] == ["ollama-qwen"]
89
+ assert body["data"][0]["owned_by"] == "coderouter"
90
+
91
+
92
+ def test_upstream_failure_falls_back_to_provider_name(
93
+ monkeypatch: pytest.MonkeyPatch,
94
+ ) -> None:
95
+ async def fake_fetch(base_url: str) -> list[str]:
96
+ raise ConnectionError("upstream down")
97
+
98
+ monkeypatch.setattr(openai_routes, "_fetch_upstream_model_ids", fake_fetch)
99
+ client = _client(_config(_passthrough_provider()), monkeypatch)
100
+
101
+ body = client.get("/v1/models").json()
102
+ assert [m["id"] for m in body["data"]] == ["llama-cpp-local"]
103
+ assert body["data"][0]["owned_by"] == "coderouter"
104
+
105
+
106
+ def test_mixed_providers_mix_entries(monkeypatch: pytest.MonkeyPatch) -> None:
107
+ async def fake_fetch(base_url: str) -> list[str]:
108
+ return ["loaded-model.gguf"]
109
+
110
+ monkeypatch.setattr(openai_routes, "_fetch_upstream_model_ids", fake_fetch)
111
+ client = _client(
112
+ _config(_passthrough_provider(), _named_provider()), monkeypatch
113
+ )
114
+
115
+ body = client.get("/v1/models").json()
116
+ assert [m["id"] for m in body["data"]] == ["loaded-model.gguf", "ollama-qwen"]
117
+
118
+
119
+ def test_upstream_ids_are_ttl_cached(monkeypatch: pytest.MonkeyPatch) -> None:
120
+ calls: list[str] = []
121
+
122
+ async def fake_fetch(base_url: str) -> list[str]:
123
+ calls.append(base_url)
124
+ return ["loaded-model.gguf"]
125
+
126
+ monkeypatch.setattr(openai_routes, "_fetch_upstream_model_ids", fake_fetch)
127
+ client = _client(_config(_passthrough_provider()), monkeypatch)
128
+
129
+ client.get("/v1/models")
130
+ client.get("/v1/models")
131
+ assert len(calls) == 1, "second probe within the TTL must hit the cache"
132
+
133
+
134
+ def test_multiple_upstream_ids_all_listed(
135
+ monkeypatch: pytest.MonkeyPatch,
136
+ ) -> None:
137
+ async def fake_fetch(base_url: str) -> list[str]:
138
+ return ["model-a.gguf", "model-b.gguf"]
139
+
140
+ monkeypatch.setattr(openai_routes, "_fetch_upstream_model_ids", fake_fetch)
141
+ client = _client(_config(_passthrough_provider()), monkeypatch)
142
+
143
+ body = client.get("/v1/models").json()
144
+ assert [m["id"] for m in body["data"]] == ["model-a.gguf", "model-b.gguf"]
File without changes