coderouter-cli 2.9.1__tar.gz → 2.9.2__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 (272) hide show
  1. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/CHANGELOG.md +36 -0
  2. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/PKG-INFO +1 -1
  3. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/config/schemas.py +56 -2
  4. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/launcher.en.md +100 -0
  5. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/launcher.md +100 -0
  6. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/pyproject.toml +1 -1
  7. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_launcher_swap.py +174 -15
  8. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/.gitignore +0 -0
  9. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/LICENSE +0 -0
  10. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/README.en.md +0 -0
  11. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/README.md +0 -0
  12. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/__init__.py +0 -0
  13. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/__main__.py +0 -0
  14. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/adapters/__init__.py +0 -0
  15. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/adapters/anthropic_native.py +0 -0
  16. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/adapters/base.py +0 -0
  17. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/adapters/openai_compat.py +0 -0
  18. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/adapters/registry.py +0 -0
  19. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/cli.py +0 -0
  20. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/cli_stats.py +0 -0
  21. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/config/__init__.py +0 -0
  22. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/config/capability_registry.py +0 -0
  23. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/config/env_file.py +0 -0
  24. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/config/loader.py +0 -0
  25. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/cost.py +0 -0
  26. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/data/__init__.py +0 -0
  27. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/data/model-capabilities.yaml +0 -0
  28. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/doctor.py +0 -0
  29. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/doctor_apply.py +0 -0
  30. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/env_security.py +0 -0
  31. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/errors.py +0 -0
  32. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/gguf_introspect.py +0 -0
  33. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/__init__.py +0 -0
  34. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/_fingerprint.py +0 -0
  35. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/backend_health.py +0 -0
  36. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/context_budget.py +0 -0
  37. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/continuous_probe.py +0 -0
  38. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/drift_actions.py +0 -0
  39. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/drift_detection.py +0 -0
  40. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/memory_budget.py +0 -0
  41. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/memory_pressure.py +0 -0
  42. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/self_healing.py +0 -0
  43. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/tool_loop.py +0 -0
  44. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/hardware.py +0 -0
  45. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/__init__.py +0 -0
  46. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/anthropic_routes.py +0 -0
  47. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/app.py +0 -0
  48. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/dashboard_routes.py +0 -0
  49. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/launcher_routes.py +0 -0
  50. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/metrics_routes.py +0 -0
  51. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/openai_routes.py +0 -0
  52. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/language_tax.py +0 -0
  53. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/launcher_speculative.py +0 -0
  54. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/launcher_swap.py +0 -0
  55. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/logging.py +0 -0
  56. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/metrics/__init__.py +0 -0
  57. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/metrics/collector.py +0 -0
  58. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/metrics/prometheus.py +0 -0
  59. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/output_filters.py +0 -0
  60. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/plugins/__init__.py +0 -0
  61. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/plugins/base.py +0 -0
  62. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/plugins/loader.py +0 -0
  63. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/plugins/registry.py +0 -0
  64. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/routing/__init__.py +0 -0
  65. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/routing/adaptive.py +0 -0
  66. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/routing/auto_router.py +0 -0
  67. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/routing/budget.py +0 -0
  68. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/routing/capability.py +0 -0
  69. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/routing/fallback.py +0 -0
  70. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/state/__init__.py +0 -0
  71. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/state/audit_log.py +0 -0
  72. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/state/replay.py +0 -0
  73. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/state/request_log.py +0 -0
  74. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/state/store.py +0 -0
  75. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/state/suggest_rules.py +0 -0
  76. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/token_estimation.py +0 -0
  77. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/token_estimation_accurate.py +0 -0
  78. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/translation/__init__.py +0 -0
  79. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/translation/anthropic.py +0 -0
  80. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/translation/convert.py +0 -0
  81. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/translation/tool_repair.py +0 -0
  82. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/README.en.md +0 -0
  83. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/README.md +0 -0
  84. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/assets/dashboard-demo.png +0 -0
  85. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
  86. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
  87. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/external-agents.en.md +0 -0
  88. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/external-agents.md +0 -0
  89. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/gguf_dl.en.md +0 -0
  90. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/gguf_dl.md +0 -0
  91. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/hf-ollama-models.en.md +0 -0
  92. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/hf-ollama-models.md +0 -0
  93. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/install-backends.en.md +0 -0
  94. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/install-backends.md +0 -0
  95. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/launcher-quickstart.en.md +0 -0
  96. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/launcher-quickstart.md +0 -0
  97. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/llamacpp-direct.en.md +0 -0
  98. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/llamacpp-direct.md +0 -0
  99. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/lmstudio-direct.en.md +0 -0
  100. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/lmstudio-direct.md +0 -0
  101. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/verify-ollama-0.23.1.md +0 -0
  102. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/architecture.en.md +0 -0
  103. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/architecture.md +0 -0
  104. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/context-budget.en.md +0 -0
  105. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/context-budget.md +0 -0
  106. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/continuous-probing.en.md +0 -0
  107. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/continuous-probing.md +0 -0
  108. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/drift-detection.en.md +0 -0
  109. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/drift-detection.md +0 -0
  110. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/partial-stitch.en.md +0 -0
  111. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/partial-stitch.md +0 -0
  112. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/agent-cli-plugin-extraction.md +0 -0
  113. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/external-agents-adapter.md +0 -0
  114. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/launcher-model-swap.md +0 -0
  115. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/orchestration-companion.md +0 -0
  116. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  117. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/v1.6-auto-router-verification.md +0 -0
  118. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/v1.6-auto-router.md +0 -0
  119. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/free-tier-guide.en.md +0 -0
  120. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/free-tier-guide.md +0 -0
  121. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/language-tax.en.md +0 -0
  122. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/language-tax.md +0 -0
  123. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/remote-access.en.md +0 -0
  124. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/remote-access.md +0 -0
  125. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/security.en.md +0 -0
  126. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/security.md +0 -0
  127. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/subagent-routing.en.md +0 -0
  128. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/subagent-routing.md +0 -0
  129. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/troubleshooting.en.md +0 -0
  130. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/troubleshooting.md +0 -0
  131. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/usage-guide.en.md +0 -0
  132. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/usage-guide.md +0 -0
  133. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/low-memory-integration.en.md +0 -0
  134. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/low-memory-integration.md +0 -0
  135. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/openrouter-roster/CHANGES.md +0 -0
  136. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/openrouter-roster/README.md +0 -0
  137. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/openrouter-roster/latest.json +0 -0
  138. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v0.4.md +0 -0
  139. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v0.5-verify.md +0 -0
  140. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v0.5.md +0 -0
  141. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v0.6.md +0 -0
  142. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v0.7.md +0 -0
  143. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v1.0-verify.md +0 -0
  144. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v1.0.md +0 -0
  145. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/start/quickstart.en.md +0 -0
  146. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/start/quickstart.md +0 -0
  147. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/start/when-do-i-need-coderouter.en.md +0 -0
  148. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/start/when-do-i-need-coderouter.md +0 -0
  149. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/.env.example +0 -0
  150. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/README.md +0 -0
  151. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers-agent-cli.yaml +0 -0
  152. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers-multiagent.yaml +0 -0
  153. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.context-budget-test.yaml +0 -0
  154. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.llamacpp-vllm.yaml +0 -0
  155. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.nvidia-nim.yaml +0 -0
  156. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.ollama-auto-custom.yaml +0 -0
  157. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.ollama-auto.yaml +0 -0
  158. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.ollama-free-chain.yaml +0 -0
  159. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.raspberrypi.yaml +0 -0
  160. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.swap.yaml +0 -0
  161. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.yaml +0 -0
  162. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/demo_traffic.sh +0 -0
  163. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/openrouter_roster_diff.py +0 -0
  164. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/smoke_v2_2.sh +0 -0
  165. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/verify-providers.yaml +0 -0
  166. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/verify_ollama_0_23.py +0 -0
  167. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/verify_v0_5.sh +0 -0
  168. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/verify_v1_0.sh +0 -0
  169. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/__init__.py +0 -0
  170. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/conftest.py +0 -0
  171. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_adapter_anthropic.py +0 -0
  172. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_agent_cli.py +0 -0
  173. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_audit_log.py +0 -0
  174. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_auto_router.py +0 -0
  175. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_auto_router_cjk.py +0 -0
  176. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_backend_health.py +0 -0
  177. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_budget.py +0 -0
  178. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_capability.py +0 -0
  179. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_capability_degraded_payload.py +0 -0
  180. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_capability_registry.py +0 -0
  181. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_capability_registry_cache_control.py +0 -0
  182. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_capability_tool_choice.py +0 -0
  183. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_claude_code_suitability.py +0 -0
  184. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_cli.py +0 -0
  185. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_cli_bind_warning.py +0 -0
  186. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_cli_stats.py +0 -0
  187. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_config.py +0 -0
  188. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_config_shim_actions.py +0 -0
  189. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_context_budget.py +0 -0
  190. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_continuous_probe.py +0 -0
  191. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_dashboard_endpoint.py +0 -0
  192. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_doctor.py +0 -0
  193. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_doctor_apply.py +0 -0
  194. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_doctor_cache_probe.py +0 -0
  195. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_drift_actions.py +0 -0
  196. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_drift_detection.py +0 -0
  197. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_drift_detection_integration.py +0 -0
  198. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_env_file.py +0 -0
  199. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_env_security.py +0 -0
  200. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_errors.py +0 -0
  201. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_examples_yaml.py +0 -0
  202. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback.py +0 -0
  203. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_anthropic.py +0 -0
  204. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_cache_control.py +0 -0
  205. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_cache_observed.py +0 -0
  206. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_cache_strip.py +0 -0
  207. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_empty_response.py +0 -0
  208. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_misconfig_warn.py +0 -0
  209. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_paid_gate.py +0 -0
  210. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_thinking.py +0 -0
  211. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_tool_choice_emulate.py +0 -0
  212. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_h1_prometheus_drift.py +0 -0
  213. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_h2_h6_translation.py +0 -0
  214. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_h3_client_reuse.py +0 -0
  215. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_h4_h5_guards.py +0 -0
  216. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_h7_probe_restore.py +0 -0
  217. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_h8_launcher_auth.py +0 -0
  218. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_mA_routing.py +0 -0
  219. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_mB_adapters.py +0 -0
  220. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_mC_convert.py +0 -0
  221. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_mD_logging_metrics.py +0 -0
  222. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_mE_config_validation.py +0 -0
  223. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_mF_ingress.py +0 -0
  224. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_gguf_introspect.py +0 -0
  225. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_guards_tool_loop.py +0 -0
  226. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_hardware.py +0 -0
  227. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_ingress_anthropic.py +0 -0
  228. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_ingress_count_tokens.py +0 -0
  229. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_ingress_profile.py +0 -0
  230. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_language_tax.py +0 -0
  231. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_language_tax_integration.py +0 -0
  232. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_launcher_mtp.py +0 -0
  233. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_launcher_mtp_fallback.py +0 -0
  234. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_launcher_provider_sync.py +0 -0
  235. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_launcher_readiness_restart.py +0 -0
  236. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_launcher_swap_review.py +0 -0
  237. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_memory_budget.py +0 -0
  238. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_memory_pressure.py +0 -0
  239. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_cache.py +0 -0
  240. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_collector.py +0 -0
  241. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_cost.py +0 -0
  242. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_endpoint.py +0 -0
  243. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_jsonl.py +0 -0
  244. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_prometheus.py +0 -0
  245. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_prometheus_cache.py +0 -0
  246. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_models_passthrough.py +0 -0
  247. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_openai_compat.py +0 -0
  248. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_openrouter_roster_diff.py +0 -0
  249. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_output_filters.py +0 -0
  250. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_output_filters_adapters.py +0 -0
  251. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_partial_stitch.py +0 -0
  252. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_plugin_adapter.py +0 -0
  253. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_plugins_integration.py +0 -0
  254. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_plugins_loader.py +0 -0
  255. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_plugins_registry.py +0 -0
  256. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_reasoning_strip.py +0 -0
  257. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_repair_byte_fallback.py +0 -0
  258. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_request_log.py +0 -0
  259. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_role_normalization.py +0 -0
  260. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_routing_adaptive.py +0 -0
  261. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_self_healing.py +0 -0
  262. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_setup_sh.py +0 -0
  263. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_state_store.py +0 -0
  264. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_token_estimation.py +0 -0
  265. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_token_estimation_accurate.py +0 -0
  266. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_tokens_saved_metric.py +0 -0
  267. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_tool_repair.py +0 -0
  268. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_tool_repair_lenient.py +0 -0
  269. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_tool_repair_r4.py +0 -0
  270. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_toolrepair_bench.py +0 -0
  271. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_translation_anthropic.py +0 -0
  272. {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_translation_reverse.py +0 -0
@@ -9,6 +9,42 @@ are kept verbatim where the Japanese text itself is the subject).
9
9
 
10
10
  ---
11
11
 
12
+ ## [v2.9.2] — 2026-07-12 (config: no more dummy provider for swap-only setups)
13
+
14
+ ### Changed
15
+
16
+ - **`providers` / `profiles` no longer require a dummy entry for swap-only
17
+ deployments.** Both top-level lists were `min_length=1` (or effectively
18
+ required), forcing an unreachable placeholder provider/profile (e.g.
19
+ `base_url: http://127.0.0.1:9`) into any config that only serves models
20
+ through `launcher.swap`. They are now optional (`providers: []` /
21
+ `profiles: []`, or omitted entirely) whenever `launcher.swap.enabled: true`
22
+ and `launcher.swap.models` has at least one entry — the injected
23
+ `launcher-swap-<name>` profile(s) already cover routing, and their
24
+ providers are registered at runtime on first on-demand spawn. Every other
25
+ deployment shape keeps the original fail-fast-at-load guarantee (now
26
+ enforced by `CodeRouterConfig._check_providers_and_profiles_nonempty`
27
+ rather than the field-level `min_length=1` it replaces), with an error
28
+ message pointing at the swap-only exemption. See
29
+ `tests/test_launcher_swap.py`
30
+ (`test_swap_only_config_loads_with_omitted_providers_and_profiles` and
31
+ neighbors) for the minimal swap-only config shape.
32
+
33
+ ### Docs
34
+
35
+ - `docs/backends/launcher.md` / `.en.md` — added the `launcher.swap` (v2.9.1+)
36
+ reference: what it does, a minimal swap-only config (no `providers` /
37
+ `profiles`, verified against the real loader), a `launcher.swap.models[]`
38
+ field table, the Phase 2 schema-only fields, and behavioral notes
39
+ (auto-restart exclusion, streaming lease protection, catalog-miss
40
+ fallthrough, no simultaneous-load cap). Also documented the `launcher:`-level
41
+ `readiness_timeout_s` / `readiness_poll_interval_s` / `auto_restart*` fields
42
+ and the new `loading` process status in the configuration reference
43
+ section, and linked `docs/designs/launcher-model-swap.md` from "Related
44
+ docs" — closing the "残課題" noted in that design doc's §10.5.
45
+
46
+ ---
47
+
12
48
  ## [v2.9.1] — 2026-07-12 (launcher model swap Phase 1 — llama-swap-style on-demand models)
13
49
 
14
50
  **New: `launcher.swap`** — an opt-in, dependency-free equivalent of
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.9.1
3
+ Version: 2.9.2
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
@@ -1951,8 +1951,16 @@ class CodeRouterConfig(BaseModel):
1951
1951
  description="Master switch. ALLOW_PAID=false blocks all paid providers (plan.md §2.3).",
1952
1952
  )
1953
1953
  default_profile: str = Field(default="default")
1954
- providers: list[ProviderConfig] = Field(..., min_length=1)
1955
- profiles: list[FallbackChain] = Field(..., min_length=1)
1954
+ # [Unreleased]: relaxed from ``Field(..., min_length=1)`` so a
1955
+ # swap-only deployment (``launcher.swap.enabled`` with a non-empty
1956
+ # ``models`` catalog) can omit both fields entirely instead of
1957
+ # writing an unreachable dummy provider/profile just to satisfy the
1958
+ # schema. The "at least one" invariant is NOT dropped — it moves to
1959
+ # ``_check_providers_and_profiles_nonempty`` below, which still
1960
+ # fail-fasts at load for every other deployment shape (same
1961
+ # philosophy as the min_length constraint it replaces).
1962
+ providers: list[ProviderConfig] = Field(default_factory=list)
1963
+ profiles: list[FallbackChain] = Field(default_factory=list)
1956
1964
  mode_aliases: dict[str, str] = Field(
1957
1965
  default_factory=dict,
1958
1966
  description=(
@@ -2159,6 +2167,52 @@ class CodeRouterConfig(BaseModel):
2159
2167
  )
2160
2168
  return self
2161
2169
 
2170
+ @model_validator(mode="after")
2171
+ def _check_providers_and_profiles_nonempty(self) -> CodeRouterConfig:
2172
+ """[Unreleased]: enforce "at least one" unless launcher.swap covers it.
2173
+
2174
+ ``providers`` / ``profiles`` were relaxed from ``min_length=1`` to
2175
+ ``default_factory=list`` (see the field comments above) so a
2176
+ swap-only deployment can omit them — every request in that shape
2177
+ is routed through a ``launcher-swap-<name>`` profile that
2178
+ :meth:`_inject_swap_profiles_and_auto_router_rules` synthesizes
2179
+ below, and its backing provider is registered at runtime on first
2180
+ spawn (``SwapManager.register_provider``), so there is genuinely
2181
+ nothing to declare statically.
2182
+
2183
+ Runs BEFORE the swap injection (which only ever *adds* profiles,
2184
+ never providers) so it observes the operator's raw, undecorated
2185
+ input rather than the post-injection state — the injected
2186
+ profiles are not a substitute for a real provider declaration in
2187
+ every other deployment shape, only in the swap-only one this
2188
+ validator carves out.
2189
+
2190
+ Outside that carve-out, an empty ``providers`` or ``profiles``
2191
+ list has always been a load-time error (there would be nothing to
2192
+ route to); this validator keeps that fail-fast guarantee instead
2193
+ of silently trading it away when the ``min_length=1`` field
2194
+ constraint was dropped.
2195
+ """
2196
+ swap_cfg = self.launcher.swap if self.launcher is not None else None
2197
+ swap_covers_empty = (
2198
+ swap_cfg is not None and swap_cfg.enabled and bool(swap_cfg.models)
2199
+ )
2200
+ if swap_covers_empty:
2201
+ return self
2202
+ if not self.providers:
2203
+ raise ValueError(
2204
+ "providers: at least one entry is required (empty/omitted "
2205
+ "is only allowed when launcher.swap.enabled=true and "
2206
+ "launcher.swap.models has at least one entry)."
2207
+ )
2208
+ if not self.profiles:
2209
+ raise ValueError(
2210
+ "profiles: at least one entry is required (empty/omitted "
2211
+ "is only allowed when launcher.swap.enabled=true and "
2212
+ "launcher.swap.models has at least one entry)."
2213
+ )
2214
+ return self
2215
+
2162
2216
  @model_validator(mode="after")
2163
2217
  def _inject_swap_profiles_and_auto_router_rules(self) -> CodeRouterConfig:
2164
2218
  """§10 Q7 + design-deviation: bootstrap swap's dedicated profiles.
@@ -225,6 +225,88 @@ All of these are **estimates** — they don't account for other processes' memor
225
225
 
226
226
  ---
227
227
 
228
+ ## Automatic model swap (launcher.swap) — v2.9.1+
229
+
230
+ An equivalent of [llama-swap](https://github.com/mostlygeek/llama-swap)'s core loop, built into CodeRouter itself with no extra dependencies. Opt-in and disabled by default (`enabled: false`).
231
+
232
+ - Watches the request's `model` name and **starts the backing backend on demand** if it isn't already running
233
+ - **Holds the request** until the model finishes loading — the response is only returned once the process passes its readiness check (no connection-refused / 503 against a still-loading model)
234
+ - **Automatically unloads** an idle process (no in-flight requests) after `ttl_seconds`, freeing memory
235
+
236
+ ### Minimal configuration
237
+
238
+ ```yaml
239
+ # ~/.coderouter/providers.yaml
240
+ default_profile: auto
241
+
242
+ auto_router:
243
+ default_rule_profile: launcher-swap-ornith-9b
244
+ rules: []
245
+
246
+ launcher:
247
+ model_dirs:
248
+ - ~/models
249
+
250
+ swap:
251
+ enabled: true
252
+ ttl_seconds: 1800
253
+ readiness_timeout_s: 180
254
+
255
+ models:
256
+ - name: ornith-9b
257
+ backend: llama.cpp
258
+ model_path: ~/models/Ornith-1.0-9B-Q4_K_M.gguf
259
+ port: 18081
260
+ num_ctx: 32768
261
+ extra_args: "-ngl 99"
262
+
263
+ - name: qwen3-coder-30b
264
+ backend: llama.cpp
265
+ model_path: ~/models/Qwen3-Coder-30B-Q4_K_M.gguf
266
+ port: 18082
267
+ num_ctx: 32768
268
+ ```
269
+
270
+ Note there's no `providers:` / `profiles:` at all here. **As of v2.9.2**, when `launcher.swap.enabled: true` and `launcher.swap.models` has at least one entry, the top-level `providers` / `profiles` lists can be omitted (or left empty, `[]`) — a `launcher-swap-<name>` profile for each catalog model is auto-injected at config load, and its backing provider is registered at runtime on first on-demand spawn. **Through v2.9.1**, this relaxation didn't exist, so you had to write an unreachable dummy `providers` / `profiles` entry (e.g. `base_url: http://127.0.0.1:9`).
271
+
272
+ For a more realistic example that mixes swap with an always-on backend like Ollama, see [`examples/providers.swap.yaml`](../../examples/providers.swap.yaml).
273
+
274
+ ### `launcher.swap` fields
275
+
276
+ | Field | Type | Default | Description |
277
+ |---|---|---|---|
278
+ | `enabled` | bool | `false` | Enable on-demand swap. `false` preserves manual-only startup exactly as before (no impact on existing deployments) |
279
+ | `ttl_seconds` | float \| null | `1800.0` | Seconds of no in-flight requests after which the process is auto-stopped. `null` = disabled (runs until explicitly stopped), `0` = unload as soon as the last request completes. Phase 1 has a single global value only — no per-model override |
280
+ | `readiness_timeout_s` | float | `120.0` (1–1800) | Upper bound (seconds) a **request** will wait for an on-demand spawn to become ready before the dispatch hook raises a retryable `AdapterError`. **Distinct from `launcher.readiness_timeout_s` below (default 300s)** — this one bounds "how long one request waits," that one bounds "the general readiness-monitoring window for a process" |
281
+ | `sweep_interval_s` | float | `15.0` (1–600) | How often the TTL sweeper background task scans for idle processes |
282
+ | `inject_auto_router_rules` | bool | `true` | Whether to auto-generate an `auto_router` rule per catalog model (`id: swap:<name>`, `model_pattern` is an exact match on the name). Set `false` to wire routing yourself (e.g. via the `X-CodeRouter-Profile` header) |
283
+ | `models[].name` | str (required) | — | Logical name matched against the request's `model` field. The provider name and dedicated profile name are automatically `launcher-swap-<name>` |
284
+ | `models[].backend` | `"llama.cpp" \| "vllm" \| "mlx"` (required) | — | Same backend set as the manual Launcher UI |
285
+ | `models[].model_path` | str (required) | — | Absolute or `~`-relative model file path. Must be **under `launcher.model_dirs`** — re-validated both at config load and at spawn time (defense against path traversal) |
286
+ | `models[].port` | int \| null | `null` | Fixed port recommended. When unset, an OS-assigned ephemeral port is used, with one retry on a different port if startup fails (best-effort — no strong TOCTOU guarantee) |
287
+ | `models[].option_profile` | str \| null | `null` | Name of an existing preset in `launcher.option_profiles[backend]`. A name that doesn't exist is a config-load error |
288
+ | `models[].num_ctx` | int | `8192` (≥256) | Baseline used for KV estimation / launch parameters |
289
+ | `models[].extra_args` | str | `""` | One-off extra CLI flags (a single string, parsed with `shlex`; re-specifying the model/draft model is rejected) |
290
+ | `models[].draft_model_path` | str \| null | `null` | Explicit MTP/draft companion gguf |
291
+ | `models[].mtp_mode` | `"auto" \| "off"` | `"auto"` | Same meaning as the manual Launcher UI's **MTP** field |
292
+ | `models[].model_pattern` | str \| null | `null` | Additional regex (`re.fullmatch`) accepted alongside an exact match on `name`. Only affects catalog matching (`SwapManager.match`) — the auto-injected auto_router rule always uses an exact match (`re.escape`) on `name` |
293
+
294
+ > **Phase 2 fields (schema-declared only, not implemented)**: `models[].group` (`"swap" | "persistent" | "exclusive"`), `models[].est_weights_gb`, `launcher.swap.memory_budget_gb`, and `launcher.swap.max_loaded` exist in the schema but are never consulted by Phase 1 (as of v2.9.1) logic.
295
+
296
+ ### Behavioral notes
297
+
298
+ - **Swap-managed processes are excluded from `launcher.auto_restart`.** Crash recovery is handled by SwapManager's own re-spawn on the next request, rather than the generic auto-restart supervisor — avoiding two supervisors fighting over the same fixed port
299
+ - **Streaming responses are never TTL-evicted mid-stream.** A "lease" is held until the final chunk is reached, so a process is never killed while it's still generating
300
+ - **A request whose `model` name doesn't match the catalog** falls through to `auto_router.default_rule_profile` (or ordinary profile resolution) — it never reaches a swap-dedicated profile
301
+ - **There's no cap on how many models can be loaded simultaneously** (Phase 1) — as many models as fit in memory can run at once. Budgeting and eviction-based exclusive swap are planned for Phase 2
302
+ - **Omitting `providers` / `profiles` is available as of v2.9.2** (see "Minimal configuration" above). Earlier versions required an unreachable dummy provider/profile
303
+
304
+ Measured end-to-end (macOS, M3 Max, Metal, ~350MB-class GGUF): the `_run/swap-test/` automated test kit reported ALL PASS across cold spawn (~2s) → warm reuse (~0s, no re-spawn) → catalog-miss fallthrough → TTL unload → respawn. See the design doc's implementation record (§10.5) for details.
305
+
306
+ For the full design — concurrency model, security considerations, and review decisions — see [`docs/designs/launcher-model-swap.md`](../designs/launcher-model-swap.md).
307
+
308
+ ---
309
+
228
310
  ## Configuration reference
229
311
 
230
312
  The MODELS list, option profiles, and binary paths are all loaded from the `launcher:` block in `~/.coderouter/providers.yaml`. **Shared between the desktop and Web editions**.
@@ -296,6 +378,23 @@ option_profiles:
296
378
  | `bool: true` | `--flag` only (no value) |
297
379
  | `bool: false` | this flag is omitted |
298
380
 
381
+ ### Readiness gating and auto-restart (v2.9.1+)
382
+
383
+ These fields can be added directly under the `launcher:` block (same level as `model_dirs` / `backends`). They apply to both manual starts and swap's on-demand starts.
384
+
385
+ | Field | Type | Default | Description |
386
+ |---|---|---|---|
387
+ | `readiness_timeout_s` | float | `300.0` (5–3600) | Maximum seconds to wait for a launched backend to become "ready." For llama.cpp / vllm, that means `GET /health` returns 200; other backends use a bare TCP connect. If the deadline is exceeded, the process is left running but never registered as a provider, and its status becomes `error`. The 5-minute default accounts for large GGUF load times |
388
+ | `readiness_poll_interval_s` | float | `2.0` (0.2–60) | Seconds between readiness probes while a launched backend's status is `loading` |
389
+ | `auto_restart` | bool | `false` | When `true`, a backend that crashes (non-zero exit — after the one-shot MTP startup-crash fallback, if applicable, has already been tried) is automatically relaunched, up to `auto_restart_max_attempts` times with exponential backoff. An intentional stop (the Stop button, or server shutdown) is never treated as a crash. Default `false`, matching the opt-in stance of `ProviderConfig.restart_command`. **Swap-managed processes are excluded** — SwapManager is their sole supervisor |
390
+ | `auto_restart_max_attempts` | int | `3` (0–20) | Maximum consecutive auto-restart attempts before giving up. Resets to 0 once a restarted process passes its readiness check. Ignored when `auto_restart` is `false` |
391
+ | `auto_restart_backoff_s` | float | `2.0` (0.1–300) | Initial backoff (seconds) before the first auto-restart attempt. Doubles on each subsequent attempt, capped at `auto_restart_backoff_max_s` |
392
+ | `auto_restart_backoff_max_s` | float | `30.0` (1–600) | Cap (seconds) on the exponential auto-restart backoff |
393
+
394
+ **New process status `loading`** — a launched process that's still waiting on its readiness check, alongside `starting` / `running` / `stopped` / `error` in the PROCESSES table. Once readiness passes, it transitions to `running`, and only then is it registered as a provider (previously it was registered the instant the process spawned, so requests could race a model that was still loading and get connection-refused / 503).
395
+
396
+ > **Behavior change (v2.9.1)**: `POST /api/launcher/start` no longer performs provider sync synchronously — the response's `provider_sync` is always `null`. Check `/api/launcher/processes` or the `provider sync:` log line for the sync result.
397
+
299
398
  ### Extra options (free-form input)
300
399
 
301
400
  The string in the UI's "Extra options" field is parsed with `shlex.split()` and appended to the end of the command. Use this to try experimental flags not in a profile.
@@ -404,3 +503,4 @@ Restarting CodeRouter reflects it in the UI. `launcher_profiles.yaml.example` is
404
503
  - [Architecture details — Launcher section](../concepts/architecture.en.md#launcher--llamacpp--vllm-プロセス管理-v250)
405
504
  - [Usage Guide](../guides/usage-guide.md)
406
505
  - [llama.cpp direct connection guide](./llamacpp-direct.en.md)
506
+ - [Model auto-swap design doc](../designs/launcher-model-swap.md) — `launcher.swap`'s concurrency model, security considerations, and review decisions
@@ -225,6 +225,88 @@ MODELS 一覧の各モデルには、CodeRouter を動かしているマシン
225
225
 
226
226
  ---
227
227
 
228
+ ## モデル自動スワップ (launcher.swap) — v2.9.1+
229
+
230
+ [llama-swap](https://github.com/mostlygeek/llama-swap) 相当の機能を、追加の依存なしで CodeRouter 単体に組み込んだものです。既定は無効(`enabled: false`)の opt-in 機能です。
231
+
232
+ - リクエストの `model` 名を見て、まだ起動していない backend を**オンデマンドで起動**します
233
+ - モデルのロードが完了するまでリクエストは**保留**され、readiness を通過して初めて応答が返ります(ロード未完了への connection-refused / 503 は起きません)
234
+ - 進行中リクエストが無い状態(アイドル)が `ttl_seconds` 続くと**自動アンロード**してメモリを解放します
235
+
236
+ ### 最小構成
237
+
238
+ ```yaml
239
+ # ~/.coderouter/providers.yaml
240
+ default_profile: auto
241
+
242
+ auto_router:
243
+ default_rule_profile: launcher-swap-ornith-9b
244
+ rules: []
245
+
246
+ launcher:
247
+ model_dirs:
248
+ - ~/models
249
+
250
+ swap:
251
+ enabled: true
252
+ ttl_seconds: 1800
253
+ readiness_timeout_s: 180
254
+
255
+ models:
256
+ - name: ornith-9b
257
+ backend: llama.cpp
258
+ model_path: ~/models/Ornith-1.0-9B-Q4_K_M.gguf
259
+ port: 18081
260
+ num_ctx: 32768
261
+ extra_args: "-ngl 99"
262
+
263
+ - name: qwen3-coder-30b
264
+ backend: llama.cpp
265
+ model_path: ~/models/Qwen3-Coder-30B-Q4_K_M.gguf
266
+ port: 18082
267
+ num_ctx: 32768
268
+ ```
269
+
270
+ 上記は `providers:` / `profiles:` を一切書いていません。**v2.9.2 以降**、`launcher.swap.enabled: true` かつ `launcher.swap.models` が1件以上あれば、トップレベルの `providers` / `profiles` は省略(または空リスト `[]`)で構いません — カタログの各モデルに対応する `launcher-swap-<name>` プロファイルが設定ロード時に自動注入され、対応する provider は初回のオンデマンド起動時にランタイム登録されます。**v2.9.1 まで**はこの緩和が無く、`providers` / `profiles` に到達不能なダミーエントリ(例: `base_url: http://127.0.0.1:9`)を書く必要がありました。
271
+
272
+ Ollama など常用の backend と併用する、より実運用寄りの設定例は [`examples/providers.swap.yaml`](../../examples/providers.swap.yaml) を参照してください。
273
+
274
+ ### `launcher.swap` フィールド
275
+
276
+ | フィールド | 型 | 既定値 | 説明 |
277
+ |---|---|---|---|
278
+ | `enabled` | bool | `false` | オンデマンドスワップを有効化。`false` ならこれまで通り手動起動のみ(既存デプロイに影響なし) |
279
+ | `ttl_seconds` | float \| null | `1800.0` | 進行中リクエストが無い状態がこの秒数続くとプロセスを自動停止。`null` = 無効(明示停止まで起動し続ける)、`0` = 最後のリクエスト完了で即アンロード。Phase 1 はグローバル1値のみ(モデル毎の上書きは未対応) |
280
+ | `readiness_timeout_s` | float | `120.0`(1〜1800) | オンデマンド起動したモデルの準備完了を**リクエスト側が待つ**上限秒数。超えると dispatch フックが retryable な `AdapterError` を送出する。**下記の `launcher.readiness_timeout_s`(既定 300s)とは別物** — こちらは「1リクエストが待つ上限」、あちらは「プロセスの readiness 監視全般の上限」 |
281
+ | `sweep_interval_s` | float | `15.0`(1〜600) | TTL 監視(sweeper)がアイドルプロセスを走査する間隔 |
282
+ | `inject_auto_router_rules` | bool | `true` | カタログの各モデル名ごとに `auto_router` ルール(`id: swap:<name>`、`model_pattern` は名前の完全一致)を自動生成するか。`false` にした場合は `X-CodeRouter-Profile` ヘッダ等で自分でルーティングを配線する |
283
+ | `models[].name` | str(必須) | — | リクエストの `model` フィールドと照合される論理名。provider 名・専用プロファイル名は自動的に `launcher-swap-<name>` になる |
284
+ | `models[].backend` | `"llama.cpp" \| "vllm" \| "mlx"`(必須) | — | 手動 Launcher UI と同じバックエンド種別 |
285
+ | `models[].model_path` | str(必須) | — | モデルファイルの絶対または `~` 相対パス。**必ず `launcher.model_dirs` の配下**である必要があり、設定ロード時と起動時の2回検証される(パストラバーサル対策) |
286
+ | `models[].port` | int \| null | `null` | 固定ポート推奨。省略時は OS 割当のエフェメラルポートを使い、起動失敗時に別ポートで1回だけリトライする(best-effort、強い TOCTOU 保証はない) |
287
+ | `models[].option_profile` | str \| null | `null` | `launcher.option_profiles[backend]` の既存プリセット名。存在しない名前を指定すると設定ロード時にエラーになる |
288
+ | `models[].num_ctx` | int | `8192`(≥256) | KV 見積り・起動パラメータの基準値 |
289
+ | `models[].extra_args` | str | `""` | 追加 CLI フラグ(1本の文字列。`shlex` でパース。モデル/draft モデルの再指定は不可) |
290
+ | `models[].draft_model_path` | str \| null | `null` | MTP/draft companion gguf の明示指定 |
291
+ | `models[].mtp_mode` | `"auto" \| "off"` | `"auto"` | 手動 Launcher UI の **MTP** 欄と同じ意味 |
292
+ | `models[].model_pattern` | str \| null | `null` | `name` の完全一致に加えて許容する正規表現(`re.fullmatch`)。カタログ内マッチング(`SwapManager.match`)のみに影響し、自動注入される auto_router ルールは常に `name` の完全一致(`re.escape`)を使う |
293
+
294
+ > **Phase 2 フィールド(スキーマ宣言のみ・未実装)**: `models[].group`(`"swap" | "persistent" | "exclusive"`)、`models[].est_weights_gb`、`launcher.swap.memory_budget_gb`、`launcher.swap.max_loaded` はスキーマ上は存在しますが、Phase 1(v2.9.1時点)ではロジックから一切参照されません。
295
+
296
+ ### 挙動に関する注記
297
+
298
+ - **swap プロセスは `launcher.auto_restart` の対象外**です。クラッシュ時の一次的な回復は次のリクエストによる SwapManager 自身の再 spawn に委ねられ、汎用の auto-restart 監督とは重複しません(同じ固定ポートを2つの監督者が取り合う事態を避けるため)
299
+ - **ストリーミング応答中はTTLアンロードされません**。応答の最終チャンクに到達するまで「リース」を保持するため、生成途中でプロセスが停止されることはありません
300
+ - **カタログに一致しない `model` 名**のリクエストは、`auto_router.default_rule_profile`(または通常のプロファイル解決)へフォールスルーします。swap 側の専用プロファイルには到達しません
301
+ - **同時ロード数に上限はありません**(Phase 1)。メモリに載る範囲で複数モデルを同時に起動できます。予算管理・退避を伴う排他 swap は Phase 2 で予定
302
+ - **`providers` / `profiles` の省略は v2.9.2 から**です(上記「最小構成」参照)。それ以前のバージョンでは到達不能なダミーの provider/profile を用意する必要がありました
303
+
304
+ 実機検証(macOS / M3 Max / Metal、350MB級 GGUF): `_run/swap-test/` の自動テストキットで cold spawn(約2秒)→ warm reuse(約0秒、再 spawn なし)→ catalog-miss フォールスルー → TTL unload → respawn の一連が ALL PASS。詳細は設計書の実装記録(§10.5)を参照してください。
305
+
306
+ 並行性モデル・セキュリティ考慮・レビュー決定事項などの設計の詳細は [`docs/designs/launcher-model-swap.md`](../designs/launcher-model-swap.md) を参照してください。
307
+
308
+ ---
309
+
228
310
  ## 設定リファレンス
229
311
 
230
312
  MODELS 一覧・オプションプロファイル・バイナリパスは `~/.coderouter/providers.yaml` の `launcher:` ブロックから読み込まれます。**デスクトップ版・Web版で共通**です。
@@ -296,6 +378,23 @@ option_profiles:
296
378
  | `bool: true` | `--flag` のみ(値なし) |
297
379
  | `bool: false` | このフラグを省略 |
298
380
 
381
+ ### readiness ゲーティングと自動再起動(v2.9.1+)
382
+
383
+ `launcher:` ブロック直下(`model_dirs` / `backends` と同じ階層)に以下のフィールドを追加できます。手動起動・swap によるオンデマンド起動の両方が対象です。
384
+
385
+ | フィールド | 型 | 既定値 | 説明 |
386
+ |---|---|---|---|
387
+ | `readiness_timeout_s` | float | `300.0`(5〜3600) | 起動した backend が「準備完了」になるまで待つ最大秒数。llama.cpp / vllm は `GET /health` が 200 を返すこと、それ以外の backend は TCP 接続成功をもって判定する。期限を超えるとプロセスは動かしたまま provider 登録はされず、status が `error` になる。既定 5 分は大きな GGUF のロード時間を見込んだ値 |
388
+ | `readiness_poll_interval_s` | float | `2.0`(0.2〜60) | status が `loading` の間、readiness を再確認する間隔(秒) |
389
+ | `auto_restart` | bool | `false` | `true` にすると、クラッシュ(0 以外の終了コード。MTP起動クラッシュの1回限りの自動フォールバックは別途優先して実行される)した backend を、`auto_restart_max_attempts` 回まで指数バックオフで自動再起動する。Stop ボタンによる意図的停止やサーバーシャットダウンはクラッシュ扱いにならない。既定 `false`(`ProviderConfig.restart_command` と同じ opt-in 方針)。**swap が管理するプロセスは対象外**(SwapManager が唯一の監督者) |
390
+ | `auto_restart_max_attempts` | int | `3`(0〜20) | 自動再起動の最大連続試行回数。再起動したプロセスが readiness を通過すると 0 にリセットされる。`auto_restart: false` のときは無視される |
391
+ | `auto_restart_backoff_s` | float | `2.0`(0.1〜300) | 最初の自動再起動までの初期バックオフ秒数。試行のたびに倍増し `auto_restart_backoff_max_s` で頭打ちになる |
392
+ | `auto_restart_backoff_max_s` | float | `30.0`(1〜600) | 自動再起動バックオフの上限秒数 |
393
+
394
+ **新しいプロセス status `loading`** — 起動直後で readiness 待ち中の状態です。`starting` / `running` / `stopped` / `error` に加えて PROCESSES テーブルに表示されます。readiness を通過すると `running` に遷移し、そこで初めて provider として登録されます(以前は spawn 直後に登録していたため、ロード未完了へのリクエストが connection-refused / 503 になる不具合がありました)。
395
+
396
+ > **動作変更(v2.9.1)**: `POST /api/launcher/start` は provider 同期を同期的に行わなくなりました。レスポンスの `provider_sync` は常に `null` です。同期結果は `/api/launcher/processes` またはログの `provider sync:` 行で確認してください。
397
+
299
398
  ### 追加オプション(自由入力)
300
399
 
301
400
  UI の「追加オプション」欄の文字列は `shlex.split()` でパースされ、コマンド末尾に追加されます。プロファイルに無い実験的なフラグを試すときに使います。
@@ -404,3 +503,4 @@ CodeRouter を再起動すると UI に反映されます。`launcher_profiles.y
404
503
  - [アーキテクチャ詳細 — Launcher セクション](../concepts/architecture.md#launcher--llamacpp--vllm-プロセス管理-v250)
405
504
  - [利用ガイド](../guides/usage-guide.md)
406
505
  - [llama.cpp 直接接続ガイド](./llamacpp-direct.md)
506
+ - [モデル自動スワップ 設計ドキュメント](../designs/launcher-model-swap.md) — `launcher.swap` の並行性モデル・セキュリティ考慮・レビュー決定事項
@@ -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.9.1"
14
+ version = "2.9.2"
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"
@@ -442,15 +442,30 @@ def test_swap_model_provider_and_profile_names() -> None:
442
442
 
443
443
 
444
444
  def _base_config_kwargs(swap_cfg: LauncherSwapConfig) -> dict[str, Any]:
445
+ """Swap-only config kwargs — ``providers``/``profiles`` start empty.
446
+
447
+ [Unreleased]: demonstrates the relaxed ``CodeRouterConfig`` schema
448
+ (``_check_providers_and_profiles_nonempty``) instead of the old
449
+ ``min_length=1``-driven dummy ``local`` provider / ``default``
450
+ profile placeholder that earlier revisions of this fixture carried
451
+ purely to satisfy the schema — a genuinely swap-only deployment
452
+ needs neither. ``default_profile`` is ``"auto"`` (the only value
453
+ exempt from the pre-injection default_profile-exists check) paired
454
+ with an explicit, empty-rules ``auto_router`` pointed at the first
455
+ swap model's to-be-injected profile — this sidesteps the bundled
456
+ ruleset merge path (which would otherwise demand unrelated
457
+ multi/coding/writing profiles), exactly like the documented
458
+ swap-only example config.
459
+ """
460
+ from coderouter.config.schemas import AutoRouterConfig
461
+
462
+ target_profile = swap_cfg.models[0].profile_name if swap_cfg.models else "default"
445
463
  return dict(
446
464
  allow_paid=False,
447
- default_profile="default",
448
- providers=[
449
- ProviderConfig(
450
- name="local", base_url="http://localhost:8080/v1", model="qwen"
451
- )
452
- ],
453
- profiles=[FallbackChain(name="default", providers=["local"])],
465
+ default_profile="auto",
466
+ providers=[],
467
+ profiles=[],
468
+ auto_router=AutoRouterConfig(rules=[], default_rule_profile=target_profile),
454
469
  launcher=LauncherConfig(model_dirs=["/tmp"], swap=swap_cfg),
455
470
  )
456
471
 
@@ -473,6 +488,9 @@ def test_swap_injects_placeholder_profile_and_auto_router_rule() -> None:
473
488
  swap_cfg = LauncherSwapConfig(enabled=True, models=[_spec(name="qwen-coder-14b")])
474
489
  cfg = CodeRouterConfig(**_base_config_kwargs(swap_cfg))
475
490
 
491
+ # No static provider/profile was declared at all — swap alone fills
492
+ # both in (the provider is registered at runtime on first spawn).
493
+ assert cfg.providers == []
476
494
  profile = cfg.profile_by_name("launcher-swap-qwen-coder-14b")
477
495
  assert profile.providers == []
478
496
 
@@ -485,12 +503,31 @@ def test_swap_injects_placeholder_profile_and_auto_router_rule() -> None:
485
503
 
486
504
 
487
505
  def test_swap_rule_injection_disabled_by_flag() -> None:
506
+ """Needs a real ``default`` profile (unlike ``_base_config_kwargs``):
507
+
508
+ with ``inject_auto_router_rules=False`` swap never builds an
509
+ ``auto_router`` block, so ``default_profile: auto`` without one would
510
+ fall onto the *bundled* ruleset's multi/coding/writing requirement —
511
+ an unrelated concern this test isn't exercising. A normal static
512
+ profile sidesteps that, independent of the omission feature under
513
+ test elsewhere in this module.
514
+ """
488
515
  swap_cfg = LauncherSwapConfig(
489
516
  enabled=True,
490
517
  inject_auto_router_rules=False,
491
518
  models=[_spec(name="qwen-coder-14b")],
492
519
  )
493
- cfg = CodeRouterConfig(**_base_config_kwargs(swap_cfg))
520
+ cfg = CodeRouterConfig(
521
+ allow_paid=False,
522
+ default_profile="default",
523
+ providers=[
524
+ ProviderConfig(
525
+ name="local", base_url="http://localhost:8080/v1", model="qwen"
526
+ )
527
+ ],
528
+ profiles=[FallbackChain(name="default", providers=["local"])],
529
+ launcher=LauncherConfig(model_dirs=["/tmp"], swap=swap_cfg),
530
+ )
494
531
  # Profile still pre-declared (needed for manual routing)...
495
532
  cfg.profile_by_name("launcher-swap-qwen-coder-14b")
496
533
  # ...but no rule was generated.
@@ -498,24 +535,146 @@ def test_swap_rule_injection_disabled_by_flag() -> None:
498
535
 
499
536
 
500
537
  def test_swap_rules_appended_after_user_auto_router_rules() -> None:
538
+ """Needs a real ``default`` profile (unlike ``_base_config_kwargs``):
539
+
540
+ the test asserts the user-supplied ``default_rule_profile: "default"``
541
+ passes through untouched, which requires ``"default"`` to actually be
542
+ a declared profile (``_check_auto_router_profiles_exist`` would
543
+ otherwise reject it) — independent of the omission feature under
544
+ test elsewhere in this module.
545
+ """
501
546
  from coderouter.config.schemas import AutoRouterConfig, AutoRouteRule, RuleMatcher
502
547
 
503
548
  swap_cfg = LauncherSwapConfig(enabled=True, models=[_spec(name="qwen-coder-14b")])
504
- kwargs = _base_config_kwargs(swap_cfg)
505
- kwargs["auto_router"] = AutoRouterConfig(
506
- rules=[
507
- AutoRouteRule(
508
- id="user:my-rule", profile="default", match=RuleMatcher(has_image=True)
549
+ cfg = CodeRouterConfig(
550
+ allow_paid=False,
551
+ default_profile="default",
552
+ providers=[
553
+ ProviderConfig(
554
+ name="local", base_url="http://localhost:8080/v1", model="qwen"
509
555
  )
510
556
  ],
511
- default_rule_profile="default",
557
+ profiles=[FallbackChain(name="default", providers=["local"])],
558
+ auto_router=AutoRouterConfig(
559
+ rules=[
560
+ AutoRouteRule(
561
+ id="user:my-rule", profile="default", match=RuleMatcher(has_image=True)
562
+ )
563
+ ],
564
+ default_rule_profile="default",
565
+ ),
566
+ launcher=LauncherConfig(model_dirs=["/tmp"], swap=swap_cfg),
512
567
  )
513
- cfg = CodeRouterConfig(**kwargs)
514
568
  ids = [r.id for r in cfg.auto_router.rules]
515
569
  assert ids == ["user:my-rule", "swap:qwen-coder-14b"]
516
570
  assert cfg.auto_router.default_rule_profile == "default" # left untouched
517
571
 
518
572
 
573
+ # ---------------------------------------------------------------------------
574
+ # B2. [Unreleased]: swap-only minimal config — no dummy providers/profiles
575
+ # ---------------------------------------------------------------------------
576
+
577
+
578
+ def _write_swap_only_yaml(tmp_path: Path, *, include_empty_lists: bool) -> Path:
579
+ """The documented swap-only minimal config (top-level ``providers``/
580
+ ``profiles`` entirely omitted, or present-but-empty — both must load).
581
+ """
582
+ model_dir = tmp_path / "models"
583
+ model_dir.mkdir()
584
+ (model_dir / "a.gguf").touch()
585
+ empty_lists = "providers: []\nprofiles: []\n" if include_empty_lists else ""
586
+ content = f"""\
587
+ default_profile: auto
588
+ {empty_lists}auto_router:
589
+ default_rule_profile: launcher-swap-m1
590
+ rules: []
591
+ launcher:
592
+ model_dirs: ["{model_dir}"]
593
+ swap:
594
+ enabled: true
595
+ models:
596
+ - name: m1
597
+ backend: llama.cpp
598
+ model_path: {model_dir / "a.gguf"}
599
+ port: 18081
600
+ """
601
+ cfg_path = tmp_path / "providers.yaml"
602
+ cfg_path.write_text(content)
603
+ return cfg_path
604
+
605
+
606
+ def test_swap_only_config_loads_with_omitted_providers_and_profiles(
607
+ tmp_path: Path,
608
+ ) -> None:
609
+ """[Unreleased]: providers/profiles can be left out of the YAML entirely
610
+ for a swap-only deployment — no unreachable dummy entry required."""
611
+ from coderouter.config.loader import load_config
612
+
613
+ cfg = load_config(_write_swap_only_yaml(tmp_path, include_empty_lists=False))
614
+ assert cfg.providers == []
615
+ assert cfg.profile_by_name("launcher-swap-m1").providers == []
616
+ assert cfg.auto_router is not None
617
+ assert cfg.auto_router.default_rule_profile == "launcher-swap-m1"
618
+
619
+
620
+ def test_swap_only_config_loads_with_explicit_empty_lists(tmp_path: Path) -> None:
621
+ """Same minimal config, but with ``providers: []`` / ``profiles: []``
622
+ spelled out explicitly — identical result to omitting them."""
623
+ from coderouter.config.loader import load_config
624
+
625
+ cfg = load_config(_write_swap_only_yaml(tmp_path, include_empty_lists=True))
626
+ assert cfg.providers == []
627
+ assert cfg.profile_by_name("launcher-swap-m1").providers == []
628
+ assert cfg.auto_router is not None
629
+ assert cfg.auto_router.default_rule_profile == "launcher-swap-m1"
630
+
631
+
632
+ def test_empty_providers_without_swap_still_rejected() -> None:
633
+ """Regression: swap disabled + empty providers -> still a load-time
634
+ ValidationError (the min_length=1 fail-fast guarantee is preserved,
635
+ just moved into ``_check_providers_and_profiles_nonempty``)."""
636
+ with pytest.raises(ValidationError, match="providers"):
637
+ CodeRouterConfig(
638
+ allow_paid=False,
639
+ default_profile="auto",
640
+ providers=[],
641
+ profiles=[FallbackChain(name="default", providers=["local"])],
642
+ auto_router=None,
643
+ )
644
+
645
+
646
+ def test_empty_profiles_without_swap_still_rejected() -> None:
647
+ """Regression: swap disabled + empty profiles -> still a load-time
648
+ ValidationError."""
649
+ with pytest.raises(ValidationError, match="profiles"):
650
+ CodeRouterConfig(
651
+ allow_paid=False,
652
+ default_profile="default",
653
+ providers=[
654
+ ProviderConfig(
655
+ name="local", base_url="http://localhost:8080/v1", model="qwen"
656
+ )
657
+ ],
658
+ profiles=[],
659
+ )
660
+
661
+
662
+ def test_empty_providers_and_profiles_rejected_when_swap_models_empty() -> None:
663
+ """Regression: swap enabled but its catalog is empty -> the relaxation
664
+ does not kick in (matches spec: models must have >=1 entry)."""
665
+ with pytest.raises(ValidationError, match="providers"):
666
+ CodeRouterConfig(
667
+ allow_paid=False,
668
+ default_profile="auto",
669
+ providers=[],
670
+ profiles=[],
671
+ auto_router=None,
672
+ launcher=LauncherConfig(
673
+ model_dirs=["/tmp"], swap=LauncherSwapConfig(enabled=True, models=[])
674
+ ),
675
+ )
676
+
677
+
519
678
  # ---------------------------------------------------------------------------
520
679
  # C. End-to-end via TestClient — real spawn, real local stub HTTP server
521
680
  # ---------------------------------------------------------------------------
File without changes
File without changes