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.
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/CHANGELOG.md +36 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/PKG-INFO +1 -1
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/config/schemas.py +56 -2
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/launcher.en.md +100 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/launcher.md +100 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/pyproject.toml +1 -1
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_launcher_swap.py +174 -15
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/.gitignore +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/LICENSE +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/README.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/README.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/adapters/anthropic_native.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/adapters/base.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/adapters/openai_compat.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/cli.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/config/loader.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/cost.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/errors.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/context_budget.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/self_healing.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/anthropic_routes.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/launcher_routes.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/ingress/openai_routes.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/launcher_speculative.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/launcher_swap.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/logging.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/routing/capability.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/routing/fallback.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/state/audit_log.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/state/request_log.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/translation/convert.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/README.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/README.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/external-agents.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/external-agents.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/gguf_dl.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/hf-ollama-models.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/launcher-quickstart.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/llamacpp-direct.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/llamacpp-direct.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/architecture.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/context-budget.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/context-budget.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/continuous-probing.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/drift-detection.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/partial-stitch.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/agent-cli-plugin-extraction.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/external-agents-adapter.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/launcher-model-swap.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/orchestration-companion.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/remote-access.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/remote-access.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/security.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/subagent-routing.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/subagent-routing.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/low-memory-integration.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/.env.example +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/README.md +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers-agent-cli.yaml +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers-multiagent.yaml +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.llamacpp-vllm.yaml +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.raspberrypi.yaml +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.swap.yaml +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/examples/providers.yaml +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/__init__.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/conftest.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_agent_cli.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_auto_router.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_budget.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_capability.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_capability_tool_choice.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_cli.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_cli_bind_warning.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_config.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_config_shim_actions.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_context_budget.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_errors.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_cache_strip.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_empty_response.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fallback_tool_choice_emulate.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_h1_prometheus_drift.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_h2_h6_translation.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_h3_client_reuse.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_h4_h5_guards.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_h7_probe_restore.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_h8_launcher_auth.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_mA_routing.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_mB_adapters.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_mC_convert.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_mD_logging_metrics.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_mE_config_validation.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_fix_mF_ingress.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_ingress_count_tokens.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_launcher_mtp.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_launcher_mtp_fallback.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_launcher_provider_sync.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_launcher_readiness_restart.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_launcher_swap_review.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_models_passthrough.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_plugin_adapter.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_tool_repair_lenient.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_tool_repair_r4.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_toolrepair_bench.py +0 -0
- {coderouter_cli-2.9.1 → coderouter_cli-2.9.2}/tests/test_translation_anthropic.py +0 -0
- {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.
|
|
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
|
-
|
|
1955
|
-
|
|
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.
|
|
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="
|
|
448
|
-
providers=[
|
|
449
|
-
|
|
450
|
-
|
|
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(
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|