coderouter-cli 2.9.2__tar.gz → 2.9.3__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.2 → coderouter_cli-2.9.3}/CHANGELOG.md +84 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/PKG-INFO +1 -1
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/config/schemas.py +41 -6
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/launcher_routes.py +60 -3
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/launcher_swap.py +100 -12
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/launcher.en.md +4 -2
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/launcher.md +4 -2
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/orchestration-companion.md +8 -7
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/pyproject.toml +1 -1
- coderouter_cli-2.9.3/tests/test_launcher_gui_readiness.py +650 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_launcher_swap.py +277 -8
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_launcher_swap_review.py +4 -2
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/.gitignore +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/LICENSE +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/README.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/README.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/adapters/anthropic_native.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/adapters/base.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/adapters/openai_compat.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/cli.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/config/loader.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/cost.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/errors.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/context_budget.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/self_healing.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/anthropic_routes.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/openai_routes.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/launcher_speculative.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/logging.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/routing/capability.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/routing/fallback.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/state/audit_log.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/state/request_log.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/translation/convert.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/README.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/README.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/external-agents.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/external-agents.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/gguf_dl.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/hf-ollama-models.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/launcher-quickstart.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/llamacpp-direct.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/llamacpp-direct.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/architecture.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/context-budget.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/context-budget.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/continuous-probing.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/drift-detection.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/partial-stitch.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/agent-cli-plugin-extraction.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/external-agents-adapter.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/launcher-model-swap.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/remote-access.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/remote-access.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/security.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/subagent-routing.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/subagent-routing.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/low-memory-integration.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/.env.example +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/README.md +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers-agent-cli.yaml +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers-multiagent.yaml +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.llamacpp-vllm.yaml +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.raspberrypi.yaml +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.swap.yaml +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.yaml +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/__init__.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/conftest.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_agent_cli.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_auto_router.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_budget.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_capability.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_capability_tool_choice.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_cli.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_cli_bind_warning.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_config.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_config_shim_actions.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_context_budget.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_errors.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_cache_strip.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_empty_response.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_tool_choice_emulate.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_h1_prometheus_drift.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_h2_h6_translation.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_h3_client_reuse.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_h4_h5_guards.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_h7_probe_restore.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_h8_launcher_auth.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_mA_routing.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_mB_adapters.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_mC_convert.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_mD_logging_metrics.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_mE_config_validation.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_mF_ingress.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_ingress_count_tokens.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_launcher_mtp.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_launcher_mtp_fallback.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_launcher_provider_sync.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_launcher_readiness_restart.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_models_passthrough.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_plugin_adapter.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_tool_repair_lenient.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_tool_repair_r4.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_toolrepair_bench.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_translation_reverse.py +0 -0
|
@@ -9,6 +9,90 @@ are kept verbatim where the Japanese text itself is the subject).
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## [v2.9.3] — 2026-07-12 (GUI/Web parity, swap polish, launcher UI fixes)
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Per-model TTL override for `launcher.swap`.** `SwapModelSpec.ttl_seconds`
|
|
17
|
+
(new, optional, `None` by default) lets a catalog entry override the
|
|
18
|
+
global `launcher.swap.ttl_seconds` for just that one model — `None`
|
|
19
|
+
keeps following the global value, `0` unloads the model as soon as
|
|
20
|
+
its last in-flight lease releases (same meaning as the global field's
|
|
21
|
+
`0`, just scoped to one entry). `SwapManager.sweep_once` now resolves
|
|
22
|
+
each model's effective TTL individually, and the sweeper starts
|
|
23
|
+
whenever either the global TTL or at least one catalog entry's
|
|
24
|
+
override is set (previously gated solely on the global value). See
|
|
25
|
+
`tests/test_launcher_swap.py` (`test_ttl_override_spec_wins_over_global`,
|
|
26
|
+
`test_ttl_override_unset_falls_back_to_global`,
|
|
27
|
+
`test_ttl_override_zero_unloads_even_when_global_ttl_disabled`) and the
|
|
28
|
+
updated `launcher.swap` field table in `docs/backends/launcher.md` /
|
|
29
|
+
`launcher.en.md`.
|
|
30
|
+
- **Configurable ephemeral-port retry count for `launcher.swap`.**
|
|
31
|
+
`LauncherSwapConfig.port_retry_attempts` (new, default `2`, range
|
|
32
|
+
0–5) replaces the previously hard-coded single retry for catalog
|
|
33
|
+
entries that leave `port` unset — the swap manager now attempts up
|
|
34
|
+
to `1 + port_retry_attempts` spawns (each on a freshly picked
|
|
35
|
+
ephemeral port) before giving up, versus the previous fixed 2 total
|
|
36
|
+
attempts. Fixed-port entries are unaffected (still exactly one
|
|
37
|
+
attempt — a second try on the same port would just collide again).
|
|
38
|
+
This narrows the practical impact of the known pick-then-bind TOCTOU
|
|
39
|
+
window documented on `SwapModelSpec.port` / `_pick_ephemeral_port`
|
|
40
|
+
but does not close it outright; a fixed `port` remains the only way
|
|
41
|
+
to eliminate the race entirely. See
|
|
42
|
+
`tests/test_launcher_swap.py`
|
|
43
|
+
(`test_port_none_retries_default_port_retry_attempts`,
|
|
44
|
+
`test_port_retry_attempts_exhausted_raises`,
|
|
45
|
+
`test_port_retry_attempts_configurable_to_zero`).
|
|
46
|
+
- **`/launcher` UI now shows which processes are swap-managed.**
|
|
47
|
+
`GET /api/launcher/processes` includes `swap_managed` (bool) and
|
|
48
|
+
`swap_model` (the swap catalog model name, or `null`) for every
|
|
49
|
+
process; `spawn_process` gained a `swap_model` kwarg that
|
|
50
|
+
`SwapManager._spawn` now passes through. The process table renders a
|
|
51
|
+
small "swap" badge next to the name of any swap-managed process
|
|
52
|
+
(title shows the backing catalog model when known). Manually-started
|
|
53
|
+
processes are unaffected (`swap_managed: false`, `swap_model: null`).
|
|
54
|
+
See `tests/test_launcher_swap.py::test_i1_on_demand_spawn_reaches_200`.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- **Swap-managed "stopped" processes no longer accumulate in the launcher
|
|
59
|
+
registry.** `stop_process` only sets `status="stopped"` and never
|
|
60
|
+
removes the entry — deliberate for manually started processes (the
|
|
61
|
+
stopped row is visible history in the /launcher UI, with logs and an
|
|
62
|
+
explicit ✕ delete button), but swap-managed processes went through the
|
|
63
|
+
same path, so every failed readiness attempt left one permanent
|
|
64
|
+
"stopped" row (`1 + port_retry_attempts` rows per failed load — 3 with
|
|
65
|
+
the defaults) and every TTL unload left another, all growing
|
|
66
|
+
`GET /api/launcher/processes` and the UI without bound. `SwapManager`
|
|
67
|
+
now removes its own processes from the registry after stopping them,
|
|
68
|
+
in both the failed-readiness cleanup (`_spawn_with_retry`) and the TTL
|
|
69
|
+
unload (`_unload_locked`), guarded on `ManagedProcess.swap_managed` so
|
|
70
|
+
a manual process is never swept up. Crash leftovers (a swap process
|
|
71
|
+
that died on its own) are deliberately kept — their log tail is the
|
|
72
|
+
only crash forensics an operator has. See `tests/test_launcher_swap.py`
|
|
73
|
+
(`test_failed_load_leaves_no_registry_litter`,
|
|
74
|
+
`test_ttl_unload_removes_registry_entry`,
|
|
75
|
+
`test_registry_removal_skips_non_swap_processes`; adapted from the
|
|
76
|
+
review repro with its assertions inverted).
|
|
77
|
+
- **/launcher UI no longer 404-polls a removed process's logs forever.**
|
|
78
|
+
The log panel's poller (`poll()` → `refreshLogs()`, every 3s) never
|
|
79
|
+
checked the response status: once the polled id left the registry —
|
|
80
|
+
a `coderouter serve` restart under a still-open browser tab, a delete
|
|
81
|
+
issued from another client, or (with the fix above) a swap TTL unload
|
|
82
|
+
removing the entry mid-view — the 404 body has no `.logs`, the
|
|
83
|
+
resulting TypeError landed in the generic catch, and `selectedLogId`
|
|
84
|
+
was never cleared, so the tab kept hitting
|
|
85
|
+
`GET /api/launcher/logs/<stale-id>` and spamming `404 Not Found` into
|
|
86
|
+
the serve log indefinitely. The UI now stops polling as soon as the
|
|
87
|
+
selected id disappears from the periodic `/api/launcher/processes`
|
|
88
|
+
refresh (usually before a single 404 is even issued), treats a logs
|
|
89
|
+
404 as terminal (shows "(process removed)" instead of retrying), and
|
|
90
|
+
the server keeps answering 404 for unknown ids — that behavior is
|
|
91
|
+
correct and now pinned by
|
|
92
|
+
`tests/test_launcher_swap.py::test_logs_unknown_proc_id_is_404`.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
12
96
|
## [v2.9.2] — 2026-07-12 (config: no more dummy provider for swap-only setups)
|
|
13
97
|
|
|
14
98
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.9.
|
|
3
|
+
Version: 2.9.3
|
|
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
|
|
@@ -1518,10 +1518,25 @@ class SwapModelSpec(BaseModel):
|
|
|
1518
1518
|
le=65535,
|
|
1519
1519
|
description=(
|
|
1520
1520
|
"Fixed port (recommended — §10 Q2). When unset, the swap "
|
|
1521
|
-
"manager picks an OS-assigned ephemeral port and retries "
|
|
1522
|
-
"
|
|
1523
|
-
"
|
|
1524
|
-
"
|
|
1521
|
+
"manager picks an OS-assigned ephemeral port and retries, "
|
|
1522
|
+
"each time on a freshly picked port, up to "
|
|
1523
|
+
"``LauncherSwapConfig.port_retry_attempts`` additional "
|
|
1524
|
+
"times if the backend fails to become ready. Best-effort "
|
|
1525
|
+
"only — no strong TOCTOU guarantee (§6.6 known-trap #4)."
|
|
1526
|
+
),
|
|
1527
|
+
)
|
|
1528
|
+
ttl_seconds: float | None = Field(
|
|
1529
|
+
default=None,
|
|
1530
|
+
ge=0.0,
|
|
1531
|
+
description=(
|
|
1532
|
+
"[Unreleased] Per-model override of "
|
|
1533
|
+
"``LauncherSwapConfig.ttl_seconds``. ``None`` (default) = "
|
|
1534
|
+
"use the global value. ``0`` = unload as soon as the last "
|
|
1535
|
+
"in-flight lease for THIS model releases — same meaning as "
|
|
1536
|
+
"the global field's ``0``, just scoped to one catalog "
|
|
1537
|
+
"entry. Lets a large/expensive model unload sooner (or "
|
|
1538
|
+
"stay resident longer) than the rest of the catalog "
|
|
1539
|
+
"without changing the global default."
|
|
1525
1540
|
),
|
|
1526
1541
|
)
|
|
1527
1542
|
option_profile: str | None = Field(
|
|
@@ -1611,8 +1626,10 @@ class LauncherSwapConfig(BaseModel):
|
|
|
1611
1626
|
"Seconds of no in-flight requests after which an idle swap "
|
|
1612
1627
|
"process is auto-stopped. None = TTL disabled (runs until "
|
|
1613
1628
|
"explicitly stopped). 0 = unload as soon as the last "
|
|
1614
|
-
"in-flight lease releases. §10 Q1:
|
|
1615
|
-
"
|
|
1629
|
+
"in-flight lease releases. §10 Q1: this is the GLOBAL "
|
|
1630
|
+
"default; a catalog entry's ``SwapModelSpec.ttl_seconds`` "
|
|
1631
|
+
"overrides it for that one model when set "
|
|
1632
|
+
"([Unreleased] per-model TTL override)."
|
|
1616
1633
|
),
|
|
1617
1634
|
)
|
|
1618
1635
|
readiness_timeout_s: float = Field(
|
|
@@ -1631,6 +1648,24 @@ class LauncherSwapConfig(BaseModel):
|
|
|
1631
1648
|
le=600.0,
|
|
1632
1649
|
description="How often the TTL sweeper background task scans for idle processes.",
|
|
1633
1650
|
)
|
|
1651
|
+
port_retry_attempts: int = Field(
|
|
1652
|
+
default=2,
|
|
1653
|
+
ge=0,
|
|
1654
|
+
le=5,
|
|
1655
|
+
description=(
|
|
1656
|
+
"[Unreleased] Number of ADDITIONAL spawn attempts (each on a "
|
|
1657
|
+
"freshly picked ephemeral port) after the first, used only "
|
|
1658
|
+
"when a catalog entry's ``port`` is unset and the previous "
|
|
1659
|
+
"attempt fails to become ready. 0 = no retry. Ignored when "
|
|
1660
|
+
"``port`` is set — a fixed port never retries (a second "
|
|
1661
|
+
"attempt would just collide on the same port again). This "
|
|
1662
|
+
"does not close the pick-then-bind TOCTOU window (see "
|
|
1663
|
+
"``coderouter.launcher_swap._pick_ephemeral_port``); it only "
|
|
1664
|
+
"bounds how many times the swap manager re-rolls the dice. "
|
|
1665
|
+
"A fixed ``port`` remains the recommended way to eliminate "
|
|
1666
|
+
"the race entirely (§10 Q2)."
|
|
1667
|
+
),
|
|
1668
|
+
)
|
|
1634
1669
|
inject_auto_router_rules: bool = Field(
|
|
1635
1670
|
default=True,
|
|
1636
1671
|
description=(
|
|
@@ -171,6 +171,13 @@ class ManagedProcess:
|
|
|
171
171
|
# under its per-model lock); a launcher auto-restart racing that
|
|
172
172
|
# re-spawn would fight over the same fixed port.
|
|
173
173
|
swap_managed: bool = False
|
|
174
|
+
# [Unreleased]: the swap catalog model name (SwapModelSpec.name) this
|
|
175
|
+
# process backs, set by SwapManager._spawn via spawn_process's
|
|
176
|
+
# swap_model kwarg. None for a manually-started process (swap_managed
|
|
177
|
+
# is False) or if a future swap-managed spawn path omits it. Purely
|
|
178
|
+
# informational — surfaced by GET /api/launcher/processes so the
|
|
179
|
+
# /launcher UI can label which catalog entry a running process is.
|
|
180
|
+
swap_model: str | None = None
|
|
174
181
|
# asyncio subprocess handle — not serialised
|
|
175
182
|
_proc: Any = field(default=None, repr=False, compare=False)
|
|
176
183
|
|
|
@@ -1027,7 +1034,13 @@ async def api_launcher_config_debug(request: Request) -> dict[str, Any]:
|
|
|
1027
1034
|
|
|
1028
1035
|
@router.get("/api/launcher/processes")
|
|
1029
1036
|
async def api_processes(request: Request) -> dict[str, Any]:
|
|
1030
|
-
"""List all managed processes.
|
|
1037
|
+
"""List all managed processes.
|
|
1038
|
+
|
|
1039
|
+
[Unreleased]: ``swap_managed`` / ``swap_model`` let the ``/launcher``
|
|
1040
|
+
UI (and other API clients) tell an on-demand SwapManager-spawned
|
|
1041
|
+
process apart from a manually-started one, and label which swap
|
|
1042
|
+
catalog model it backs.
|
|
1043
|
+
"""
|
|
1031
1044
|
reg = _registry(request)
|
|
1032
1045
|
return {
|
|
1033
1046
|
"processes": [
|
|
@@ -1040,6 +1053,8 @@ async def api_processes(request: Request) -> dict[str, Any]:
|
|
|
1040
1053
|
"status": p.status,
|
|
1041
1054
|
"pid": p.pid,
|
|
1042
1055
|
"returncode": p.returncode,
|
|
1056
|
+
"swap_managed": p.swap_managed,
|
|
1057
|
+
"swap_model": p.swap_model,
|
|
1043
1058
|
}
|
|
1044
1059
|
for p in reg.all()
|
|
1045
1060
|
]
|
|
@@ -1100,6 +1115,7 @@ async def spawn_process(
|
|
|
1100
1115
|
draft_model_path: str | None = None,
|
|
1101
1116
|
mtp_mode: str = "auto",
|
|
1102
1117
|
swap_managed: bool = False,
|
|
1118
|
+
swap_model: str | None = None,
|
|
1103
1119
|
) -> ManagedProcess:
|
|
1104
1120
|
"""Build argv, spawn the child process, and arm readiness/log tailing.
|
|
1105
1121
|
|
|
@@ -1116,6 +1132,12 @@ async def spawn_process(
|
|
|
1116
1132
|
exist. Any other exception from ``create_subprocess_exec`` propagates
|
|
1117
1133
|
as-is. Callers decide how to surface these (HTTP 400/500 for
|
|
1118
1134
|
``api_start``, a retryable ``AdapterError`` for SwapManager).
|
|
1135
|
+
|
|
1136
|
+
``swap_model`` ([Unreleased]) is purely informational — the swap
|
|
1137
|
+
catalog model name (``SwapModelSpec.name``), recorded on the
|
|
1138
|
+
resulting ``ManagedProcess`` for ``GET /api/launcher/processes`` /
|
|
1139
|
+
the ``/launcher`` UI. Only ``SwapManager._spawn`` passes it; the
|
|
1140
|
+
manual ``POST /api/launcher/start`` path leaves it ``None``.
|
|
1119
1141
|
"""
|
|
1120
1142
|
options = options if options is not None else {}
|
|
1121
1143
|
configured_binary: str | None = None
|
|
@@ -1178,6 +1200,7 @@ async def spawn_process(
|
|
|
1178
1200
|
spec_auto=spec_auto and fallback_cmd is not None,
|
|
1179
1201
|
fallback_cmd=fallback_cmd,
|
|
1180
1202
|
swap_managed=swap_managed,
|
|
1203
|
+
swap_model=swap_model,
|
|
1181
1204
|
)
|
|
1182
1205
|
proc.log_tail.append(f"[launcher] cmd: {' '.join(cmd)}")
|
|
1183
1206
|
for note in spec_notes:
|
|
@@ -1378,6 +1401,10 @@ _LAUNCHER_HTML = r"""<!doctype html>
|
|
|
1378
1401
|
<style>
|
|
1379
1402
|
.dot { width:.5rem;height:.5rem;border-radius:9999px;display:inline-block; }
|
|
1380
1403
|
.tabnum { font-variant-numeric:tabular-nums; }
|
|
1404
|
+
.badge-swap { font-size:10px; text-transform:uppercase; letter-spacing:.02em;
|
|
1405
|
+
background:rgba(99,102,241,.2); color:#a5b4fc; padding:1px 6px;
|
|
1406
|
+
border-radius:9999px; margin-left:6px; vertical-align:middle;
|
|
1407
|
+
white-space:nowrap; }
|
|
1381
1408
|
.log-box { font-family:monospace;font-size:.75rem;line-height:1.4;
|
|
1382
1409
|
overflow-y:auto;max-height:14rem;white-space:pre-wrap;word-break:break-all; }
|
|
1383
1410
|
.model-row:hover { background:rgba(255,255,255,.04);cursor:pointer; }
|
|
@@ -1851,7 +1878,17 @@ _LAUNCHER_HTML = r"""<!doctype html>
|
|
|
1851
1878
|
try {
|
|
1852
1879
|
const r = await fetch("/api/launcher/processes");
|
|
1853
1880
|
const d = await r.json();
|
|
1854
|
-
|
|
1881
|
+
const procs = d.processes || [];
|
|
1882
|
+
renderProcesses(procs);
|
|
1883
|
+
// [Unreleased] 404-loop fix: if the process whose logs we're
|
|
1884
|
+
// polling is no longer in the registry (deleted from another
|
|
1885
|
+
// client, swap TTL unload removed it, or the server restarted
|
|
1886
|
+
// under a still-open tab), stop the log polling BEFORE it even
|
|
1887
|
+
// issues a 404 — otherwise poll() would hit
|
|
1888
|
+
// /api/launcher/logs/<stale-id> every POLL_MS forever.
|
|
1889
|
+
if (selectedLogId && !procs.some(p => p.id === selectedLogId)) {
|
|
1890
|
+
markLogGone();
|
|
1891
|
+
}
|
|
1855
1892
|
} catch (_) {}
|
|
1856
1893
|
};
|
|
1857
1894
|
|
|
@@ -1871,8 +1908,14 @@ _LAUNCHER_HTML = r"""<!doctype html>
|
|
|
1871
1908
|
? `<button onclick="deleteProc('${p.id}')" class="btn-sm btn-slate ml-1">✕</button>`
|
|
1872
1909
|
: "";
|
|
1873
1910
|
const logBtn = `<button onclick="openLog('${p.id}','${esc(p.name)}')" class="btn-sm btn-indigo ml-1">📋 ログ</button>`;
|
|
1911
|
+
// [Unreleased]: swap badge — marks processes SwapManager spawned
|
|
1912
|
+
// on demand, distinct from manually-started ones. Title shows the
|
|
1913
|
+
// catalog model name when the backend supplied it.
|
|
1914
|
+
const swapBadge = p.swap_managed
|
|
1915
|
+
? `<span class="badge-swap" title="${esc(p.swap_model ? "swap model: " + p.swap_model : "swap-managed")}">swap</span>`
|
|
1916
|
+
: "";
|
|
1874
1917
|
return `<tr>
|
|
1875
|
-
<td class="py-2 pr-3 font-medium">${esc(p.name)}</td>
|
|
1918
|
+
<td class="py-2 pr-3 font-medium">${esc(p.name)}${swapBadge}</td>
|
|
1876
1919
|
<td class="py-2 pr-3 text-slate-400">${esc(p.backend)}</td>
|
|
1877
1920
|
<td class="py-2 pr-3 text-slate-400 truncate max-w-[10rem]" title="${esc(p.model_path)}">${esc(modelName)}</td>
|
|
1878
1921
|
<td class="py-2 pr-3 text-right">${p.port}</td>
|
|
@@ -1912,6 +1955,11 @@ _LAUNCHER_HTML = r"""<!doctype html>
|
|
|
1912
1955
|
if (!selectedLogId) return;
|
|
1913
1956
|
try {
|
|
1914
1957
|
const r = await fetch(`/api/launcher/logs/${selectedLogId}?n=200`);
|
|
1958
|
+
// [Unreleased] 404-loop fix: the id is gone from the registry —
|
|
1959
|
+
// stop polling instead of retrying (and 404-spamming the serve
|
|
1960
|
+
// log) every POLL_MS forever. The server keeps answering 404 for
|
|
1961
|
+
// unknown ids (correct); the client just has to take the hint.
|
|
1962
|
+
if (r.status === 404) { markLogGone(); return; }
|
|
1915
1963
|
const d = await r.json();
|
|
1916
1964
|
const box = document.getElementById("log-box");
|
|
1917
1965
|
box.textContent = d.logs.join("\n") || "(ログなし)";
|
|
@@ -1921,6 +1969,15 @@ _LAUNCHER_HTML = r"""<!doctype html>
|
|
|
1921
1969
|
}
|
|
1922
1970
|
};
|
|
1923
1971
|
|
|
1972
|
+
// [Unreleased] 404-loop fix: the process we were tailing no longer
|
|
1973
|
+
// exists server-side. Clear selectedLogId (which is what stops both
|
|
1974
|
+
// poll()'s refreshLogs call and any manual ↻) but keep the panel open
|
|
1975
|
+
// with a terminal message so the user sees WHY the logs stopped.
|
|
1976
|
+
const markLogGone = () => {
|
|
1977
|
+
selectedLogId = null;
|
|
1978
|
+
document.getElementById("log-box").textContent = "(process removed)";
|
|
1979
|
+
};
|
|
1980
|
+
|
|
1924
1981
|
window.closeLog = () => {
|
|
1925
1982
|
selectedLogId = null;
|
|
1926
1983
|
document.getElementById("log-panel").classList.add("hidden");
|
|
@@ -85,9 +85,18 @@ class _ModelState:
|
|
|
85
85
|
def _pick_ephemeral_port() -> int:
|
|
86
86
|
"""Ask the OS for a free port. Best-effort only (§6.6 known-trap #4):
|
|
87
87
|
|
|
88
|
-
there is an unavoidable TOCTOU window between this call
|
|
89
|
-
and the child actually binding
|
|
90
|
-
|
|
88
|
+
there is an unavoidable TOCTOU window between this call's bind+close
|
|
89
|
+
returning a "free" port and the child actually binding that same
|
|
90
|
+
port a moment later — nothing stops another process (or another
|
|
91
|
+
concurrent swap spawn) from grabbing it first. Retrying on a fresh
|
|
92
|
+
port when the child fails to come up
|
|
93
|
+
(``LauncherSwapConfig.port_retry_attempts``, default 2 additional
|
|
94
|
+
attempts) narrows the *impact* of losing that race but does NOT
|
|
95
|
+
close the window itself — a genuinely race-free allocation would
|
|
96
|
+
need the OS to hand the child an already-bound socket fd, which the
|
|
97
|
+
current spawn path (argv ``--port N``) does not support. Fixed
|
|
98
|
+
``port:`` in the catalog entry avoids the race entirely (§10 Q2,
|
|
99
|
+
recommended) and remains the only way to eliminate it outright.
|
|
91
100
|
"""
|
|
92
101
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
93
102
|
s.bind(("127.0.0.1", 0))
|
|
@@ -126,6 +135,13 @@ class SwapManager:
|
|
|
126
135
|
self._readiness_timeout_s = config.readiness_timeout_s
|
|
127
136
|
self._sweep_interval_s = config.sweep_interval_s
|
|
128
137
|
self._sweeper_task: asyncio.Task[None] | None = None
|
|
138
|
+
# [Unreleased] per-model TTL override: the sweeper must run
|
|
139
|
+
# whenever EITHER the global TTL is enabled OR at least one
|
|
140
|
+
# catalog entry sets its own ``ttl_seconds`` (even if the global
|
|
141
|
+
# value is None/disabled) — see _effective_ttl / start().
|
|
142
|
+
self._any_ttl_configured = self._ttl_seconds is not None or any(
|
|
143
|
+
m.ttl_seconds is not None for m in config.models
|
|
144
|
+
)
|
|
129
145
|
|
|
130
146
|
# ------------------------------------------------------------------
|
|
131
147
|
# Catalog matching
|
|
@@ -214,24 +230,40 @@ class SwapManager:
|
|
|
214
230
|
async with state.lock:
|
|
215
231
|
await self._unload_locked(state, spec, reason)
|
|
216
232
|
|
|
233
|
+
def _effective_ttl(self, spec: SwapModelSpec) -> float | None:
|
|
234
|
+
"""``spec.ttl_seconds`` if set, else the global ``self._ttl_seconds``.
|
|
235
|
+
|
|
236
|
+
[Unreleased] per-model TTL override (docs/backends/launcher.md
|
|
237
|
+
"launcher.swap fields" table). ``None`` at either level keeps
|
|
238
|
+
its usual meaning ("TTL disabled") — a spec-level override only
|
|
239
|
+
takes effect when it's not ``None``.
|
|
240
|
+
"""
|
|
241
|
+
return spec.ttl_seconds if spec.ttl_seconds is not None else self._ttl_seconds
|
|
242
|
+
|
|
217
243
|
async def sweep_once(self) -> None:
|
|
218
244
|
"""One TTL sweep pass: stop every idle, un-leased, expired model.
|
|
219
245
|
|
|
220
246
|
Takes each model's own lock independently (never more than one
|
|
221
247
|
per-model lock at a time — §6.6 known-trap #7) so a slow unload
|
|
222
|
-
for model A never delays the sweep of model B.
|
|
248
|
+
for model A never delays the sweep of model B. Each model's TTL
|
|
249
|
+
is resolved individually via :meth:`_effective_ttl` — a model
|
|
250
|
+
whose global TTL is disabled can still expire on its own
|
|
251
|
+
override, and vice versa.
|
|
223
252
|
"""
|
|
224
|
-
if self.
|
|
253
|
+
if not self._any_ttl_configured:
|
|
225
254
|
return
|
|
226
255
|
now = time.monotonic()
|
|
227
256
|
for spec in list(self._catalog.values()):
|
|
257
|
+
ttl = self._effective_ttl(spec)
|
|
258
|
+
if ttl is None:
|
|
259
|
+
continue
|
|
228
260
|
state = self._states.get(spec.name)
|
|
229
261
|
if state is None:
|
|
230
262
|
continue
|
|
231
263
|
async with state.lock:
|
|
232
264
|
if state.status != "ready" or state.in_flight > 0:
|
|
233
265
|
continue
|
|
234
|
-
if (now - state.last_used) <
|
|
266
|
+
if (now - state.last_used) < ttl:
|
|
235
267
|
continue
|
|
236
268
|
await self._unload_locked(state, spec, reason="ttl")
|
|
237
269
|
|
|
@@ -240,8 +272,14 @@ class SwapManager:
|
|
|
240
272
|
# ------------------------------------------------------------------
|
|
241
273
|
|
|
242
274
|
async def start(self) -> None:
|
|
243
|
-
"""Start the TTL sweeper background task.
|
|
244
|
-
|
|
275
|
+
"""Start the TTL sweeper background task.
|
|
276
|
+
|
|
277
|
+
No-op when TTL is disabled both globally AND on every catalog
|
|
278
|
+
entry (``self._any_ttl_configured`` — [Unreleased] per-model
|
|
279
|
+
override means a per-model ``ttl_seconds`` alone is enough to
|
|
280
|
+
need the sweeper, even with the global TTL off).
|
|
281
|
+
"""
|
|
282
|
+
if not self._any_ttl_configured or self._sweeper_task is not None:
|
|
245
283
|
return
|
|
246
284
|
self._sweeper_task = asyncio.create_task(self._sweeper_loop())
|
|
247
285
|
|
|
@@ -285,6 +323,35 @@ class SwapManager:
|
|
|
285
323
|
def _engine(self) -> Any | None:
|
|
286
324
|
return getattr(self._app.state, "engine", None)
|
|
287
325
|
|
|
326
|
+
def _remove_from_registry(self, proc_id: str) -> None:
|
|
327
|
+
"""Drop a swap-managed ``ManagedProcess`` from the launcher registry.
|
|
328
|
+
|
|
329
|
+
[Unreleased] registry-litter fix: ``stop_process`` only sets
|
|
330
|
+
``status="stopped"`` — it never removes the entry, because for
|
|
331
|
+
MANUALLY started processes the stopped row doubles as visible
|
|
332
|
+
history in the /launcher UI (with logs and an explicit ✕ delete
|
|
333
|
+
button). Swap-managed processes have no such contract: every
|
|
334
|
+
failed readiness attempt (times ``1 + port_retry_attempts``) and
|
|
335
|
+
every TTL unload would otherwise leave a permanent "stopped" row
|
|
336
|
+
accumulating unboundedly in ``GET /api/launcher/processes``. So
|
|
337
|
+
SwapManager removes ITS OWN processes right after it stops them,
|
|
338
|
+
guarded on ``swap_managed`` so a manual process can never be
|
|
339
|
+
swept up by mistake. Crash leftovers (a swap process that died
|
|
340
|
+
on its own, status "error"/"stopped" without SwapManager
|
|
341
|
+
stopping it) are deliberately NOT removed here — their log tail
|
|
342
|
+
is the only crash forensics an operator has; the ✕ button still
|
|
343
|
+
applies.
|
|
344
|
+
"""
|
|
345
|
+
reg = _registry_for_app(self._app)
|
|
346
|
+
try:
|
|
347
|
+
proc = reg.get(proc_id)
|
|
348
|
+
except KeyError:
|
|
349
|
+
return
|
|
350
|
+
if not proc.swap_managed:
|
|
351
|
+
return
|
|
352
|
+
with contextlib.suppress(KeyError):
|
|
353
|
+
reg.remove(proc_id)
|
|
354
|
+
|
|
288
355
|
def _resolve_options(self, spec: SwapModelSpec) -> dict[str, Any]:
|
|
289
356
|
"""Resolve ``spec.option_profile`` into the launcher options dict."""
|
|
290
357
|
if not spec.option_profile or self._launcher_cfg is None:
|
|
@@ -346,11 +413,20 @@ class SwapManager:
|
|
|
346
413
|
async def _spawn_with_retry(
|
|
347
414
|
self, spec: SwapModelSpec
|
|
348
415
|
) -> tuple[ManagedProcess | None, Exception | None]:
|
|
349
|
-
"""Spawn + wait for readiness.
|
|
350
|
-
|
|
351
|
-
|
|
416
|
+
"""Spawn + wait for readiness.
|
|
417
|
+
|
|
418
|
+
§10 Q2: when ``spec.port`` is unset, up to
|
|
419
|
+
``1 + LauncherSwapConfig.port_retry_attempts`` attempts total
|
|
420
|
+
(default: 1 initial + 2 retries = 3), each on a freshly picked
|
|
421
|
+
ephemeral port via :func:`_pick_ephemeral_port` — see that
|
|
422
|
+
function's docstring for the residual pick-then-bind TOCTOU
|
|
423
|
+
window this does NOT close, only bounds the impact of. Fixed
|
|
424
|
+
ports never retry — a second attempt on the same port would
|
|
425
|
+
just collide again.
|
|
352
426
|
"""
|
|
353
|
-
attempts =
|
|
427
|
+
attempts = (
|
|
428
|
+
1 if spec.port is not None else 1 + self._config.port_retry_attempts
|
|
429
|
+
)
|
|
354
430
|
last_exc: Exception | None = None
|
|
355
431
|
for _attempt in range(attempts):
|
|
356
432
|
port = spec.port if spec.port is not None else _pick_ephemeral_port()
|
|
@@ -368,6 +444,10 @@ class SwapManager:
|
|
|
368
444
|
)
|
|
369
445
|
with contextlib.suppress(Exception):
|
|
370
446
|
await stop_process(self._app, proc.id)
|
|
447
|
+
# [Unreleased] registry-litter fix: without this, every failed
|
|
448
|
+
# readiness attempt leaves one "stopped" row in the registry
|
|
449
|
+
# forever (1 + port_retry_attempts rows per failed load).
|
|
450
|
+
self._remove_from_registry(proc.id)
|
|
371
451
|
return None, last_exc
|
|
372
452
|
|
|
373
453
|
async def _spawn(self, spec: SwapModelSpec, port: int) -> ManagedProcess:
|
|
@@ -394,6 +474,10 @@ class SwapManager:
|
|
|
394
474
|
# provider). H-2: exempt from launcher auto-restart (crash
|
|
395
475
|
# recovery = next-request re-spawn under the per-model lock).
|
|
396
476
|
swap_managed=True,
|
|
477
|
+
# [Unreleased]: catalog model name, surfaced by
|
|
478
|
+
# GET /api/launcher/processes as "swap_model" so the /launcher
|
|
479
|
+
# UI can show which swap catalog entry a process backs.
|
|
480
|
+
swap_model=spec.name,
|
|
397
481
|
)
|
|
398
482
|
|
|
399
483
|
async def _await_ready(self, proc: ManagedProcess) -> bool:
|
|
@@ -457,6 +541,10 @@ class SwapManager:
|
|
|
457
541
|
with contextlib.suppress(Exception):
|
|
458
542
|
await stop_process(self._app, proc_id)
|
|
459
543
|
await self._deregister(spec)
|
|
544
|
+
# [Unreleased] registry-litter fix: a TTL-unloaded swap process
|
|
545
|
+
# is not "history" the way a manually stopped one is — leaving
|
|
546
|
+
# it would grow the registry by one row per load/unload cycle.
|
|
547
|
+
self._remove_from_registry(proc_id)
|
|
460
548
|
logger.info(
|
|
461
549
|
"swap-unload",
|
|
462
550
|
extra={"model": spec.name, "reason": reason, "proc_id": proc_id},
|
|
@@ -276,14 +276,16 @@ For a more realistic example that mixes swap with an always-on backend like Olla
|
|
|
276
276
|
| Field | Type | Default | Description |
|
|
277
277
|
|---|---|---|---|
|
|
278
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.
|
|
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. This is the global default shared by all models — `models[].ttl_seconds` overrides it per model ([Unreleased]) |
|
|
280
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
281
|
| `sweep_interval_s` | float | `15.0` (1–600) | How often the TTL sweeper background task scans for idle processes |
|
|
282
|
+
| `port_retry_attempts` | int | `2` (0–5) | [Unreleased] Number of additional retries (each on a freshly picked ephemeral port) after a failed startup, for catalog entries that leave `models[].port` unset. `0` = no retry. Irrelevant for a fixed `port` (always exactly one attempt). Does not close the TOCTOU window itself — see `models[].port` below |
|
|
282
283
|
| `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
284
|
| `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
285
|
| `models[].backend` | `"llama.cpp" \| "vllm" \| "mlx"` (required) | — | Same backend set as the manual Launcher UI |
|
|
285
286
|
| `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,
|
|
287
|
+
| `models[].port` | int \| null | `null` | Fixed port recommended. When unset, an OS-assigned ephemeral port is used, retrying on a fresh port up to `launcher.swap.port_retry_attempts` times (default 2) if startup fails. Best-effort — the gap between picking the port and the child actually binding it (TOCTOU) is never fully closed. Use a fixed port if you need a strong guarantee |
|
|
288
|
+
| `models[].ttl_seconds` | float \| null | `null` | [Unreleased] Per-model TTL override. `null` (default) = follow the global `launcher.swap.ttl_seconds`. `0` means the same thing as the global field's `0` (unload as soon as the last lease releases), scoped to just this model |
|
|
287
289
|
| `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
290
|
| `models[].num_ctx` | int | `8192` (≥256) | Baseline used for KV estimation / launch parameters |
|
|
289
291
|
| `models[].extra_args` | str | `""` | One-off extra CLI flags (a single string, parsed with `shlex`; re-specifying the model/draft model is rejected) |
|
|
@@ -276,14 +276,16 @@ Ollama など常用の backend と併用する、より実運用寄りの設定
|
|
|
276
276
|
| フィールド | 型 | 既定値 | 説明 |
|
|
277
277
|
|---|---|---|---|
|
|
278
278
|
| `enabled` | bool | `false` | オンデマンドスワップを有効化。`false` ならこれまで通り手動起動のみ(既存デプロイに影響なし) |
|
|
279
|
-
| `ttl_seconds` | float \| null | `1800.0` | 進行中リクエストが無い状態がこの秒数続くとプロセスを自動停止。`null` = 無効(明示停止まで起動し続ける)、`0` =
|
|
279
|
+
| `ttl_seconds` | float \| null | `1800.0` | 進行中リクエストが無い状態がこの秒数続くとプロセスを自動停止。`null` = 無効(明示停止まで起動し続ける)、`0` = 最後のリクエスト完了で即アンロード。全モデル共通のグローバル既定値 — `models[].ttl_seconds` で個別上書き可能([Unreleased]) |
|
|
280
280
|
| `readiness_timeout_s` | float | `120.0`(1〜1800) | オンデマンド起動したモデルの準備完了を**リクエスト側が待つ**上限秒数。超えると dispatch フックが retryable な `AdapterError` を送出する。**下記の `launcher.readiness_timeout_s`(既定 300s)とは別物** — こちらは「1リクエストが待つ上限」、あちらは「プロセスの readiness 監視全般の上限」 |
|
|
281
281
|
| `sweep_interval_s` | float | `15.0`(1〜600) | TTL 監視(sweeper)がアイドルプロセスを走査する間隔 |
|
|
282
|
+
| `port_retry_attempts` | int | `2`(0〜5) | [Unreleased] `models[].port` が未指定のカタログエントリに対する、起動失敗時の追加リトライ回数(毎回新しいエフェメラルポートを取り直す)。`0` ならリトライなし。固定ポート指定時は無関係(常に1回のみ)。TOCTOU窓自体は解消しない — 詳細は `models[].port` の説明を参照 |
|
|
282
283
|
| `inject_auto_router_rules` | bool | `true` | カタログの各モデル名ごとに `auto_router` ルール(`id: swap:<name>`、`model_pattern` は名前の完全一致)を自動生成するか。`false` にした場合は `X-CodeRouter-Profile` ヘッダ等で自分でルーティングを配線する |
|
|
283
284
|
| `models[].name` | str(必須) | — | リクエストの `model` フィールドと照合される論理名。provider 名・専用プロファイル名は自動的に `launcher-swap-<name>` になる |
|
|
284
285
|
| `models[].backend` | `"llama.cpp" \| "vllm" \| "mlx"`(必須) | — | 手動 Launcher UI と同じバックエンド種別 |
|
|
285
286
|
| `models[].model_path` | str(必須) | — | モデルファイルの絶対または `~` 相対パス。**必ず `launcher.model_dirs` の配下**である必要があり、設定ロード時と起動時の2回検証される(パストラバーサル対策) |
|
|
286
|
-
| `models[].port` | int \| null | `null` | 固定ポート推奨。省略時は OS
|
|
287
|
+
| `models[].port` | int \| null | `null` | 固定ポート推奨。省略時は OS 割当のエフェメラルポートを使い、起動失敗時に `launcher.swap.port_retry_attempts` 回まで(既定2回)別ポートでリトライする。best-effort — pick してから子プロセスが実際に bind するまでの間隙(TOCTOU)は解消されない。強い保証が要る場合は固定ポートを使うこと |
|
|
288
|
+
| `models[].ttl_seconds` | float \| null | `null` | [Unreleased] このモデルだけの TTL 上書き。`null`(既定) = グローバルの `launcher.swap.ttl_seconds` に従う。`0` を指定するとグローバルの `0` と同じ意味(最後のリース解放で即アンロード)がこのモデルにだけ適用される |
|
|
287
289
|
| `models[].option_profile` | str \| null | `null` | `launcher.option_profiles[backend]` の既存プリセット名。存在しない名前を指定すると設定ロード時にエラーになる |
|
|
288
290
|
| `models[].num_ctx` | int | `8192`(≥256) | KV 見積り・起動パラメータの基準値 |
|
|
289
291
|
| `models[].extra_args` | str | `""` | 追加 CLI フラグ(1本の文字列。`shlex` でパース。モデル/draft モデルの再指定は不可) |
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# オーケストレーション・コンパニオン設計書(Ecosystem 層、構想)
|
|
2
2
|
|
|
3
|
+
> 編集注記(2026-07-12): リンク整理。`future.md` は現リポジトリに非収録のため、リンクは注記化または現存ドキュメントへの付け替え済み(記述内容自体は変更していない)。
|
|
3
4
|
> ステータス: **構想(Concept)**。実装前。Core/Plugin の変更は伴わない。
|
|
4
|
-
> 正典:
|
|
5
|
+
> 正典: `future.md`(当時の内部設計メモ。現リポジトリには未収録)§5(2026-06-27 追記)・§1.2(三層モデル)・§2.5(2026-07-11 追記)。§1.2 の判定基準に相当する記述は [`agent-cli-plugin-extraction.md`](./agent-cli-plugin-extraction.md) §1.3、§2.5 のopusplan構成例・既知の制限(G1/G6)に相当する記述は [`subagent-routing.md`](../guides/subagent-routing.md) §5(a)・§7 に移設されている。
|
|
5
6
|
> 前提: agent_cli Phase 1 完結(v2.7.10、[`external-agents-adapter.md`](./external-agents-adapter.md))
|
|
6
7
|
> 作成: 2026-07-11 / 出典: 作者方向性指示 `_article/direction-brief-2026-07-11.md`・解析書 `_article/analysis-direction-2026-07.md` D-補足
|
|
7
8
|
|
|
@@ -60,7 +61,7 @@ Voice Bridge と同型で、CodeRouter Plugin SDK は使わない。future.md §
|
|
|
60
61
|
[推論バックエンド] — agent_cli(claude opus 等) / ローカル / クラウド混在
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
`providers.yaml` 側の構成例は future.md §2.5「opusplan
|
|
64
|
+
`providers.yaml` 側の構成例は [`subagent-routing.md`](../guides/subagent-routing.md) §5(a)「opusplan 型」を参照(本書では重複させない。旧 future.md §2.5「opusplan 型構成例」に相当する記述。future.md は当時の内部設計メモで、現リポジトリには未収録)。
|
|
64
65
|
|
|
65
66
|
---
|
|
66
67
|
|
|
@@ -71,10 +72,10 @@ Voice Bridge と同型で、CodeRouter Plugin SDK は使わない。future.md §
|
|
|
71
72
|
- 役割別 profile 解決、agent_cli backend、fallback、L1〜L6 guards、plugin-memory/compress の透過適用は**すべて既存資産で足りる**。追加実装は不要(future.md §2.5 (1) 確信度 HIGH)。
|
|
72
73
|
- `X-CodeRouter-Profile` ヘッダの precedence 機構は v1.6 以降既存。
|
|
73
74
|
|
|
74
|
-
**G1/G3
|
|
75
|
+
**G1/G3(当時の内部設計メモ future.md §2.5 (4) のギャップ表より。現リポジトリには未収録。G1 は現存記述として [`subagent-routing.md`](../guides/subagent-routing.md) §7 に対応項目あり)が欲しくなる条件**:
|
|
75
76
|
|
|
76
|
-
- G1(複合条件マッチャ): orchestrator が profile ヘッダを明示付与している限り不要。**auto_router の保険ルールを複雑化させたい場合のみ**(例: 「コード密度が高く *かつ* ツール宣言あり」を 1 ルールで表現したい)に欲しくなる。orchestrator
|
|
77
|
-
- G3(ヘッダ/ツール名ベースのルール条件): 同上。orchestrator が特定 MCP ツール名に応じて動的に profile
|
|
77
|
+
- G1(複合条件マッチャ): orchestrator が profile ヘッダを明示付与している限り不要。**auto_router の保険ルールを複雑化させたい場合のみ**(例: 「コード密度が高く *かつ* ツール宣言あり」を 1 ルールで表現したい)に欲しくなる。orchestrator 側が常にヘッダを付ける設計なら発生しない(cf. [`subagent-routing.md`](../guides/subagent-routing.md) §7 の G1 記述)。
|
|
78
|
+
- G3(ヘッダ/ツール名ベースのルール条件): 同上。orchestrator が特定 MCP ツール名に応じて動的に profile を切り替えたい場合に初めて必要になる(future.md 由来。現リポジトリには対応する記述なし)。
|
|
78
79
|
|
|
79
80
|
**明確に CodeRouter 側に要求しないもの**: 段階判定(今が Plan か実行かの意味的分類、G2)・深度連動 routing(G4)・並列 fan-out(G5)はすべて orchestrator 側の責務であり、wire 層に持ち込まない。
|
|
80
81
|
|
|
@@ -104,6 +105,6 @@ Voice Bridge と同型で、CodeRouter Plugin SDK は使わない。future.md §
|
|
|
104
105
|
|
|
105
106
|
## 関連ドキュメント
|
|
106
107
|
|
|
107
|
-
- [`docs/
|
|
108
|
+
- `future.md` §5・§1.2・§2.5 — 正典(当時の内部設計メモ future.md より。現リポジトリには未収録)。§1.2 相当は [`docs/designs/agent-cli-plugin-extraction.md`](./agent-cli-plugin-extraction.md) §1.3、§2.5 の一部(opusplan構成例・G1/G6)相当は [`docs/guides/subagent-routing.md`](../guides/subagent-routing.md) §5(a)・§7 を参照
|
|
108
109
|
- [`docs/designs/external-agents-adapter.md`](./external-agents-adapter.md) — agent_cli backend(本書が利用する CodeRouter 側の対応機能)
|
|
109
|
-
- `_OUTPUTS/04-計画-方向性/multiagent/` — 実機プロトタイプ一式(orchestrator/safe-edit
|
|
110
|
+
- `_OUTPUTS/04-計画-方向性/multiagent/` — 実機プロトタイプ一式(orchestrator/safe-edit/ベンチ。当時の作業ディレクトリ参照。現リポジトリには未収録)
|
|
@@ -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.3"
|
|
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"
|