coderouter-cli 2.7.5__tar.gz → 2.7.6__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.7.5 → coderouter_cli-2.7.6}/CHANGELOG.md +69 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/PKG-INFO +1 -1
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/gguf_introspect.py +19 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/ingress/launcher_routes.py +215 -20
- coderouter_cli-2.7.6/coderouter/launcher_speculative.py +333 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/launcher-quickstart.en.md +2 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/launcher-quickstart.md +2 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/launcher.en.md +55 -1
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/launcher.md +55 -1
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/llamacpp-direct.en.md +26 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/llamacpp-direct.md +26 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/guides/remote-access.en.md +6 -6
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/guides/remote-access.md +6 -6
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/examples/providers.llamacpp-vllm.yaml +7 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/pyproject.toml +1 -1
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_gguf_introspect.py +25 -0
- coderouter_cli-2.7.6/tests/test_launcher_mtp.py +440 -0
- coderouter_cli-2.7.6/tests/test_launcher_mtp_fallback.py +272 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/.gitignore +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/LICENSE +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/README.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/README.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/adapters/anthropic_native.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/adapters/base.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/adapters/openai_compat.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/cli.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/config/loader.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/config/schemas.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/cost.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/errors.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/guards/context_budget.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/guards/self_healing.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/ingress/anthropic_routes.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/ingress/openai_routes.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/logging.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/routing/capability.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/routing/fallback.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/state/audit_log.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/state/request_log.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/translation/convert.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/README.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/README.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/gguf_dl.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/hf-ollama-models.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/concepts/architecture.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/concepts/context-budget.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/concepts/context-budget.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/concepts/continuous-probing.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/concepts/drift-detection.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/concepts/partial-stitch.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/guides/security.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/low-memory-integration.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/examples/.env.example +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/examples/README.md +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/examples/providers-multiagent.yaml +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/examples/providers.raspberrypi.yaml +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/examples/providers.yaml +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/__init__.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/conftest.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_auto_router.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_budget.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_capability.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_capability_tool_choice.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_cli.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_cli_bind_warning.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_config.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_config_shim_actions.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_context_budget.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_errors.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fallback_cache_strip.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fallback_empty_response.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fallback_tool_choice_emulate.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fix_h1_prometheus_drift.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fix_h2_h6_translation.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fix_h3_client_reuse.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fix_h4_h5_guards.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fix_h7_probe_restore.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fix_h8_launcher_auth.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fix_mA_routing.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fix_mB_adapters.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fix_mC_convert.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fix_mD_logging_metrics.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fix_mE_config_validation.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_fix_mF_ingress.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_ingress_count_tokens.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_launcher_provider_sync.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_models_passthrough.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_tool_repair_lenient.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_tool_repair_r4.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_toolrepair_bench.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.7.5 → coderouter_cli-2.7.6}/tests/test_translation_reverse.py +0 -0
|
@@ -9,6 +9,75 @@ are kept verbatim where the Japanese text itself is the subject).
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## [v2.7.6] — 2026-07-09 (Launcher MTP / speculative-decoding support)
|
|
13
|
+
|
|
14
|
+
**PR #59 / #60**. Adds Multi-Token Prediction (MTP) / speculative-decoding
|
|
15
|
+
support to the llama.cpp launcher (both the Web UI and the desktop GUI), closing the
|
|
16
|
+
gap where operators had to hand-assemble `--spec-type` / `--model-draft`
|
|
17
|
+
flags. Fully backward compatible: the new fields default to auto-detection
|
|
18
|
+
with a silent no-op fallback, no config-schema changes, no new dependencies.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **`resolve_speculative()` / `find_draft_companion()`**
|
|
23
|
+
(`coderouter/launcher_speculative.py`) — shared decision logic for both
|
|
24
|
+
launchers. `mtp_mode="auto"` (default) first checks the selected gguf's
|
|
25
|
+
own metadata for embedded nextn layers (`{arch}.nextn_predict_layers > 0`)
|
|
26
|
+
and, if present, emits `--spec-type draft-mtp` with no separate draft
|
|
27
|
+
model; otherwise it scans the *same folder* as the main gguf for a
|
|
28
|
+
companion draft/MTP file (name hint or shared prefix, size under 50% of
|
|
29
|
+
the main model, architecture-matched when readable) and wires up
|
|
30
|
+
`--spec-type draft-mtp|draft-simple --model-draft <path>`; otherwise it
|
|
31
|
+
starts normally and logs that no companion was found. An explicit
|
|
32
|
+
`draft_model_path` skips detection entirely (400 if the path doesn't
|
|
33
|
+
exist); `mtp_mode="off"` never emits speculative flags. When the operator
|
|
34
|
+
supplies `--spec-type` in extra args, auto-detection defers to it and
|
|
35
|
+
adds nothing.
|
|
36
|
+
Both knobs are llama.cpp-only — vllm/mlx reject them with a 400.
|
|
37
|
+
- **`POST /api/launcher/start`**: new optional `draft_model_path` and
|
|
38
|
+
`mtp_mode` (`"auto"` / `"off"`) fields; the response now carries the
|
|
39
|
+
resolved flags as `"speculative"`. `-md` / `--model-draft` /
|
|
40
|
+
`--spec-draft-model` join the existing `-m`/`--model` denylist for
|
|
41
|
+
`options`/`extra_args` (llama.cpp) — the draft model can only be set via
|
|
42
|
+
`draft_model_path`. The remaining spec knobs (`--spec-type`,
|
|
43
|
+
`--spec-draft-n-max/-n-min/-p-min`, `-ngld`, `-devd`) stay free-form.
|
|
44
|
+
- **GGUF introspection** (`coderouter/gguf_introspect.py`): `GGUFInfo` now
|
|
45
|
+
reports `n_nextn` and the derived `supports_mtp` property, read from
|
|
46
|
+
`{arch}.nextn_predict_layers`.
|
|
47
|
+
- **`--split-mode tensor` crash warning**: known llama.cpp issue #24309 —
|
|
48
|
+
a nextn-embedded model combined with tensor split can crash. The launcher
|
|
49
|
+
detects the combination and logs a warning recommending `--split-mode
|
|
50
|
+
layer` without blocking the launch.
|
|
51
|
+
- **UI**: Web LAUNCH form gets "MTP/draft gguf (空欄で自動検出)" text field
|
|
52
|
+
+ "MTP" `auto`/`off` select. Desktop GUI (`launcher_gui.py`) gets the
|
|
53
|
+
matching Entry + a "MTP自動検出" checkbox (default on).
|
|
54
|
+
- **Automatic MTP startup-crash fallback**: when speculative flags were added
|
|
55
|
+
by AUTO detection (`mtp_mode="auto"`, no explicit `draft_model_path`) and the
|
|
56
|
+
backend dies during startup (non-zero exit within ~3 min), the launcher
|
|
57
|
+
relaunches it ONCE without the speculative flags and logs
|
|
58
|
+
`[launcher] MTP startup failure detected (exit code ...); retrying without
|
|
59
|
+
speculative decoding` plus the flag-free command. This covers architectures
|
|
60
|
+
whose `draft-mtp` support in llama.cpp is still immature (detection is right
|
|
61
|
+
but the MTP context fails to initialize, e.g. `failed to measure MTP context
|
|
62
|
+
memory`). Explicit `draft_model_path` / operator-supplied `--spec-type` are
|
|
63
|
+
never auto-retried, and the retry can never loop (single-shot, guarded).
|
|
64
|
+
Implemented in both the Web launcher (`launcher_routes.py`) and the desktop
|
|
65
|
+
GUI (`launcher_gui.py`).
|
|
66
|
+
|
|
67
|
+
### Docs
|
|
68
|
+
|
|
69
|
+
- `docs/backends/launcher.md`/`.en.md`: new "MTP / speculative decoding"
|
|
70
|
+
section (auto-detection order, companion-file convention, `mtp_mode=off`,
|
|
71
|
+
extra-args deferral, `-md` denylist, issue #24309 caveat, API fields), plus
|
|
72
|
+
the LAUNCH form field table and denylist note updated.
|
|
73
|
+
- `docs/backends/llamacpp-direct.md`/`.en.md`: manual `--spec-type
|
|
74
|
+
draft-mtp` / `--model-draft` / `--spec-draft-n-max` usage example, with a
|
|
75
|
+
pointer to the launcher's auto-detection.
|
|
76
|
+
- `docs/backends/launcher-quickstart.md`/`.en.md`: one-line pointer to the
|
|
77
|
+
new MTP section.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
12
81
|
## [v2.7.5] — 2026-07-06 (External-bind startup warning + remote-access guide + docs i18n)
|
|
13
82
|
|
|
14
83
|
Driven by the first field report against the v2.7.0 Host-validation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.6
|
|
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
|
|
@@ -136,6 +136,17 @@ class GGUFInfo:
|
|
|
136
136
|
n_kv_heads: int | None
|
|
137
137
|
file_type: int | None
|
|
138
138
|
file_size_bytes: int
|
|
139
|
+
n_nextn: int | None = None
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def supports_mtp(self) -> bool:
|
|
143
|
+
"""True when the GGUF embeds Multi-Token-Prediction (nextn) layers.
|
|
144
|
+
|
|
145
|
+
Derived from ``{arch}.nextn_predict_layers`` — a positive count means
|
|
146
|
+
the main model carries MTP/nextn tensors and can drive llama.cpp's
|
|
147
|
+
``--spec-type draft-mtp`` without a separate draft gguf.
|
|
148
|
+
"""
|
|
149
|
+
return bool(self.n_nextn and self.n_nextn > 0)
|
|
139
150
|
|
|
140
151
|
@property
|
|
141
152
|
def quant_name(self) -> str | None:
|
|
@@ -224,6 +235,10 @@ _KEY_BLOCK_COUNT = ".block_count"
|
|
|
224
235
|
_KEY_EMBED_LEN = ".embedding_length"
|
|
225
236
|
_KEY_HEAD_COUNT = ".attention.head_count"
|
|
226
237
|
_KEY_HEAD_COUNT_KV = ".attention.head_count_kv"
|
|
238
|
+
# Number of Multi-Token-Prediction (nextn) layers embedded in the main model
|
|
239
|
+
# (e.g. ``glm4moe.nextn_predict_layers``). A positive value means the GGUF can
|
|
240
|
+
# drive llama.cpp speculative decoding via ``--spec-type draft-mtp`` alone.
|
|
241
|
+
_KEY_NEXTN = ".nextn_predict_layers"
|
|
227
242
|
|
|
228
243
|
|
|
229
244
|
def read_gguf_metadata(path: str | Path) -> GGUFInfo:
|
|
@@ -245,6 +260,7 @@ def read_gguf_metadata(path: str | Path) -> GGUFInfo:
|
|
|
245
260
|
n_heads: int | None = None
|
|
246
261
|
n_kv_heads: int | None = None
|
|
247
262
|
file_type: int | None = None
|
|
263
|
+
n_nextn: int | None = None
|
|
248
264
|
|
|
249
265
|
with p.open("rb") as fh:
|
|
250
266
|
magic = fh.read(4)
|
|
@@ -275,6 +291,8 @@ def read_gguf_metadata(path: str | Path) -> GGUFInfo:
|
|
|
275
291
|
n_kv_heads = value
|
|
276
292
|
elif key.endswith(_KEY_HEAD_COUNT) and isinstance(value, int):
|
|
277
293
|
n_heads = value
|
|
294
|
+
elif key.endswith(_KEY_NEXTN) and isinstance(value, int):
|
|
295
|
+
n_nextn = value
|
|
278
296
|
|
|
279
297
|
return GGUFInfo(
|
|
280
298
|
architecture=arch,
|
|
@@ -284,6 +302,7 @@ def read_gguf_metadata(path: str | Path) -> GGUFInfo:
|
|
|
284
302
|
n_kv_heads=n_kv_heads,
|
|
285
303
|
file_type=file_type,
|
|
286
304
|
file_size_bytes=file_size,
|
|
305
|
+
n_nextn=n_nextn,
|
|
287
306
|
)
|
|
288
307
|
|
|
289
308
|
|
|
@@ -30,6 +30,7 @@ import secrets
|
|
|
30
30
|
import shlex
|
|
31
31
|
import shutil
|
|
32
32
|
import subprocess
|
|
33
|
+
import time
|
|
33
34
|
import uuid
|
|
34
35
|
from collections import deque
|
|
35
36
|
from dataclasses import dataclass, field
|
|
@@ -40,6 +41,7 @@ from fastapi import APIRouter, HTTPException, Request
|
|
|
40
41
|
from fastapi.responses import HTMLResponse
|
|
41
42
|
from pydantic import BaseModel
|
|
42
43
|
|
|
44
|
+
from coderouter.launcher_speculative import resolve_speculative
|
|
43
45
|
from coderouter.logging import get_logger
|
|
44
46
|
|
|
45
47
|
logger = get_logger(__name__)
|
|
@@ -115,9 +117,23 @@ class ManagedProcess:
|
|
|
115
117
|
options: dict[str, Any]
|
|
116
118
|
extra_args: str
|
|
117
119
|
status: str = "starting" # "starting" | "running" | "stopped" | "error"
|
|
120
|
+
# MTP / speculative-decoding controls (defaults keep existing call sites
|
|
121
|
+
# working). Recorded for introspection; the resolved flags live in the cmd.
|
|
122
|
+
draft_model_path: str | None = None
|
|
123
|
+
mtp_mode: str = "auto"
|
|
118
124
|
pid: int | None = None
|
|
119
125
|
returncode: int | None = None
|
|
120
126
|
log_tail: deque = field(default_factory=lambda: deque(maxlen=200))
|
|
127
|
+
# MTP auto-fallback state. When the speculative flags were added by AUTO
|
|
128
|
+
# detection and the child dies during startup, the process is relaunched
|
|
129
|
+
# ONCE without them (some archs' draft-mtp support in llama.cpp is
|
|
130
|
+
# immature and crashes the context init). These fields make that possible
|
|
131
|
+
# and impossible to loop.
|
|
132
|
+
spec_tokens: list = field(default_factory=list)
|
|
133
|
+
spec_auto: bool = False
|
|
134
|
+
mtp_fallback_done: bool = False
|
|
135
|
+
fallback_cmd: list | None = None
|
|
136
|
+
started_at: float = 0.0
|
|
121
137
|
# asyncio subprocess handle — not serialised
|
|
122
138
|
_proc: Any = field(default=None, repr=False, compare=False)
|
|
123
139
|
|
|
@@ -208,8 +224,15 @@ _BACKEND_DEFAULTS: dict[str, str] = {
|
|
|
208
224
|
# - llama.cpp: ``-m`` / ``--model`` select the GGUF file.
|
|
209
225
|
# - vllm / mlx: ``--model`` selects the model; ``-m`` selects the python
|
|
210
226
|
# module that ``_build_cmd`` pins, so it must not be re-specified either.
|
|
227
|
+
# - llama.cpp: the draft/MTP model is likewise set only via the dedicated
|
|
228
|
+
# ``draft_model_path`` field, so its aliases (``-md`` / ``--model-draft``
|
|
229
|
+
# / ``--spec-draft-model``) are rejected in options / extra_args too. The
|
|
230
|
+
# remaining spec knobs (``--spec-type``, ``--spec-draft-n-max`` …) stay
|
|
231
|
+
# free-form.
|
|
211
232
|
_MODEL_FLAGS: dict[str, frozenset[str]] = {
|
|
212
|
-
"llama.cpp": frozenset(
|
|
233
|
+
"llama.cpp": frozenset(
|
|
234
|
+
{"-m", "--model", "-md", "--model-draft", "--spec-draft-model"}
|
|
235
|
+
),
|
|
213
236
|
"vllm": frozenset({"-m", "--model"}),
|
|
214
237
|
"mlx": frozenset({"-m", "--model"}),
|
|
215
238
|
}
|
|
@@ -407,6 +430,24 @@ def _resolve_within_model_dirs(model_path: str, model_dirs: list[str]) -> Path:
|
|
|
407
430
|
raise ValueError("model_path is not under any configured model_dirs.")
|
|
408
431
|
|
|
409
432
|
|
|
433
|
+
def _option_tokens(options: dict[str, Any]) -> list[str]:
|
|
434
|
+
"""Flatten an ``options`` dict into CLI tokens (``{flag: val}`` semantics).
|
|
435
|
+
|
|
436
|
+
Boolean values become a bare flag when true and vanish when false;
|
|
437
|
+
everything else becomes ``[flag, str(val)]``. Shared by ``_build_cmd`` and
|
|
438
|
+
``api_start`` so the tokens fed to ``resolve_speculative`` match exactly
|
|
439
|
+
what lands on the command line (no double-parsing drift).
|
|
440
|
+
"""
|
|
441
|
+
tokens: list[str] = []
|
|
442
|
+
for flag, val in options.items():
|
|
443
|
+
if isinstance(val, bool):
|
|
444
|
+
if val:
|
|
445
|
+
tokens.append(flag)
|
|
446
|
+
else:
|
|
447
|
+
tokens.extend([flag, str(val)])
|
|
448
|
+
return tokens
|
|
449
|
+
|
|
450
|
+
|
|
410
451
|
def _build_cmd(
|
|
411
452
|
backend: str,
|
|
412
453
|
model_path: str,
|
|
@@ -414,17 +455,27 @@ def _build_cmd(
|
|
|
414
455
|
options: dict[str, Any],
|
|
415
456
|
extra_args: str,
|
|
416
457
|
binary: str | None = None,
|
|
458
|
+
spec_tokens: list[str] | None = None,
|
|
417
459
|
) -> list[str]:
|
|
418
460
|
"""Build the CLI command list for the given backend and options.
|
|
419
461
|
|
|
420
462
|
``binary`` overrides the default executable (``llama-server`` /
|
|
421
463
|
``python``). When None, the default is used and PATH resolution
|
|
422
464
|
is left to the OS.
|
|
465
|
+
|
|
466
|
+
``spec_tokens`` are pre-resolved speculative-decoding / MTP flags
|
|
467
|
+
(from :func:`coderouter.launcher_speculative.resolve_speculative`).
|
|
468
|
+
For llama.cpp they are appended right after the port args and BEFORE
|
|
469
|
+
the profile / extra args. They are trusted (the launcher, not the
|
|
470
|
+
caller, produced them) so they bypass the model-override guard even
|
|
471
|
+
though they may contain ``--model-draft``.
|
|
423
472
|
"""
|
|
424
473
|
exe = _resolve_binary(backend, binary)
|
|
425
474
|
|
|
426
475
|
if backend == "llama.cpp":
|
|
427
476
|
cmd: list[str] = [exe, "-m", model_path, "--port", str(port)]
|
|
477
|
+
if spec_tokens:
|
|
478
|
+
cmd.extend(spec_tokens)
|
|
428
479
|
elif backend == "vllm":
|
|
429
480
|
cmd = [
|
|
430
481
|
exe, "-m", "vllm.entrypoints.openai.api_server",
|
|
@@ -446,12 +497,7 @@ def _build_cmd(
|
|
|
446
497
|
# H8: reject model-path re-specification via options keys.
|
|
447
498
|
_assert_no_model_override(backend, list(options.keys()))
|
|
448
499
|
|
|
449
|
-
|
|
450
|
-
if isinstance(val, bool):
|
|
451
|
-
if val:
|
|
452
|
-
cmd.append(flag)
|
|
453
|
-
else:
|
|
454
|
-
cmd.extend([flag, str(val)])
|
|
500
|
+
cmd.extend(_option_tokens(options))
|
|
455
501
|
|
|
456
502
|
if extra_args.strip():
|
|
457
503
|
extra_tokens = shlex.split(extra_args)
|
|
@@ -474,12 +520,37 @@ def _build_cmd(
|
|
|
474
520
|
# recover by reading the buffered chunk and continuing so log tailing survives.
|
|
475
521
|
_LOG_STREAM_LIMIT = 256 * 1024 # 256 KB
|
|
476
522
|
|
|
523
|
+
# Window (seconds) after spawn in which a non-zero exit is treated as a
|
|
524
|
+
# startup crash eligible for the MTP auto-fallback. A backend that ran fine
|
|
525
|
+
# for longer and then died is a normal crash, not a load-time failure, so it
|
|
526
|
+
# is never retried.
|
|
527
|
+
_MTP_FALLBACK_WINDOW_SECS = 180.0
|
|
477
528
|
|
|
478
|
-
|
|
479
|
-
|
|
529
|
+
|
|
530
|
+
def _should_mtp_fallback(proc: ManagedProcess) -> bool:
|
|
531
|
+
"""True when a just-crashed process qualifies for the one-shot MTP retry.
|
|
532
|
+
|
|
533
|
+
Only auto-detected speculative launches that die within the startup
|
|
534
|
+
window are eligible, and only once (guarded by ``mtp_fallback_done``).
|
|
535
|
+
"""
|
|
480
536
|
p = proc._proc
|
|
481
|
-
if p is None
|
|
482
|
-
|
|
537
|
+
rc = p.returncode if p is not None else proc.returncode
|
|
538
|
+
return (
|
|
539
|
+
proc.spec_auto
|
|
540
|
+
and not proc.mtp_fallback_done
|
|
541
|
+
and bool(proc.fallback_cmd)
|
|
542
|
+
and rc not in (0, None)
|
|
543
|
+
and (time.monotonic() - proc.started_at) <= _MTP_FALLBACK_WINDOW_SECS
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
async def _tail_logs(proc: ManagedProcess) -> None:
|
|
548
|
+
"""Read stdout+stderr into proc.log_tail until the process exits.
|
|
549
|
+
|
|
550
|
+
Loops so that a one-shot MTP fallback (relaunch without the auto-detected
|
|
551
|
+
speculative flags) can be tailed by the same task. The loop exits as soon
|
|
552
|
+
as no fallback applies.
|
|
553
|
+
"""
|
|
483
554
|
|
|
484
555
|
async def _drain(stream: asyncio.StreamReader | None) -> None:
|
|
485
556
|
if stream is None:
|
|
@@ -505,14 +576,53 @@ async def _tail_logs(proc: ManagedProcess) -> None:
|
|
|
505
576
|
break
|
|
506
577
|
proc.log_tail.append(line.decode(errors="replace").rstrip())
|
|
507
578
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
579
|
+
while True:
|
|
580
|
+
p = proc._proc
|
|
581
|
+
if p is None:
|
|
582
|
+
return
|
|
583
|
+
|
|
584
|
+
await asyncio.gather(_drain(p.stdout), _drain(p.stderr))
|
|
585
|
+
await p.wait()
|
|
586
|
+
proc.returncode = p.returncode
|
|
587
|
+
proc.pid = None
|
|
588
|
+
proc.status = "stopped" if (p.returncode or 0) == 0 else "error"
|
|
589
|
+
proc.log_tail.append(
|
|
590
|
+
f"[launcher] process exited with code {p.returncode}"
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
if not _should_mtp_fallback(proc):
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
# Auto-detected MTP crashed during startup — retry ONCE without the
|
|
597
|
+
# speculative flags. The port is unchanged, so the existing provider
|
|
598
|
+
# registration still stands (do not re-register).
|
|
599
|
+
rc = proc.returncode
|
|
600
|
+
proc.mtp_fallback_done = True
|
|
601
|
+
proc.log_tail.append(
|
|
602
|
+
f"[launcher] MTP startup failure detected (exit code {rc}); "
|
|
603
|
+
"retrying without speculative decoding"
|
|
604
|
+
)
|
|
605
|
+
proc.log_tail.append(
|
|
606
|
+
f"[launcher] cmd: {' '.join(proc.fallback_cmd)}"
|
|
607
|
+
)
|
|
608
|
+
try:
|
|
609
|
+
p = await asyncio.create_subprocess_exec(
|
|
610
|
+
*proc.fallback_cmd,
|
|
611
|
+
stdout=asyncio.subprocess.PIPE,
|
|
612
|
+
stderr=asyncio.subprocess.PIPE,
|
|
613
|
+
limit=_LOG_STREAM_LIMIT,
|
|
614
|
+
)
|
|
615
|
+
except (FileNotFoundError, OSError) as exc:
|
|
616
|
+
proc.log_tail.append(f"[launcher] fallback relaunch failed: {exc}")
|
|
617
|
+
proc.status = "error"
|
|
618
|
+
return
|
|
619
|
+
proc._proc = p
|
|
620
|
+
proc.pid = p.pid
|
|
621
|
+
proc.status = "running"
|
|
622
|
+
proc.returncode = None
|
|
623
|
+
proc.started_at = time.monotonic()
|
|
624
|
+
proc.log_tail.append(f"[launcher] started PID {p.pid}")
|
|
625
|
+
# Loop to tail the relaunched process.
|
|
516
626
|
|
|
517
627
|
|
|
518
628
|
async def shutdown_launcher(app: Any) -> None:
|
|
@@ -555,6 +665,11 @@ class StartRequest(BaseModel):
|
|
|
555
665
|
port: int
|
|
556
666
|
options: dict[str, Any] = {}
|
|
557
667
|
extra_args: str = ""
|
|
668
|
+
# MTP / speculative-decoding controls (llama.cpp only). ``draft_model_path``
|
|
669
|
+
# is an explicit companion draft/MTP gguf; ``mtp_mode`` is "auto" (detect)
|
|
670
|
+
# or "off" (never emit speculative flags).
|
|
671
|
+
draft_model_path: str | None = None
|
|
672
|
+
mtp_mode: str = "auto"
|
|
558
673
|
|
|
559
674
|
|
|
560
675
|
# ---------------------------------------------------------------------------
|
|
@@ -690,15 +805,69 @@ async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
|
|
|
690
805
|
if bc and bc.binary:
|
|
691
806
|
configured_binary = bc.binary
|
|
692
807
|
|
|
808
|
+
# Validate the MTP mode up front (only "auto" / "off" are accepted).
|
|
809
|
+
if req.mtp_mode not in ("auto", "off"):
|
|
810
|
+
raise HTTPException(
|
|
811
|
+
status_code=400,
|
|
812
|
+
detail=f"mtp_mode must be 'auto' or 'off', got {req.mtp_mode!r}.",
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
# Build the user token list once (options flags + extra_args) so
|
|
816
|
+
# resolve_speculative sees exactly what _build_cmd will emit. shlex.split
|
|
817
|
+
# can raise on unbalanced quotes → surface as 400 like other bad input.
|
|
818
|
+
try:
|
|
819
|
+
user_tokens = _option_tokens(req.options)
|
|
820
|
+
if req.extra_args.strip():
|
|
821
|
+
user_tokens += shlex.split(req.extra_args)
|
|
822
|
+
except ValueError as exc:
|
|
823
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
824
|
+
|
|
825
|
+
# Resolve MTP / speculative-decoding flags (llama.cpp only; no-op elsewhere).
|
|
826
|
+
try:
|
|
827
|
+
spec_tokens, spec_notes = resolve_speculative(
|
|
828
|
+
req.backend,
|
|
829
|
+
req.model_path,
|
|
830
|
+
req.draft_model_path,
|
|
831
|
+
req.mtp_mode,
|
|
832
|
+
user_tokens,
|
|
833
|
+
)
|
|
834
|
+
except ValueError as exc:
|
|
835
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
836
|
+
|
|
693
837
|
try:
|
|
694
838
|
cmd = _build_cmd(
|
|
695
839
|
req.backend, req.model_path, req.port,
|
|
696
840
|
req.options, req.extra_args,
|
|
697
841
|
binary=configured_binary,
|
|
842
|
+
spec_tokens=spec_tokens,
|
|
698
843
|
)
|
|
699
844
|
except ValueError as exc:
|
|
700
845
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
701
846
|
|
|
847
|
+
# Speculative flags qualify for the one-shot startup-crash fallback only
|
|
848
|
+
# when they came from AUTO detection (mtp_mode="auto", no explicit draft
|
|
849
|
+
# model, and detection actually emitted flags). Explicit draft paths and
|
|
850
|
+
# operator-supplied --spec-type are never auto-retried. The fallback cmd
|
|
851
|
+
# is rebuilt from scratch with spec_tokens=None (exact — never spliced).
|
|
852
|
+
spec_auto = (
|
|
853
|
+
req.mtp_mode == "auto"
|
|
854
|
+
and req.draft_model_path is None
|
|
855
|
+
and bool(spec_tokens)
|
|
856
|
+
)
|
|
857
|
+
fallback_cmd: list[str] | None = None
|
|
858
|
+
if spec_auto:
|
|
859
|
+
try:
|
|
860
|
+
fallback_cmd = _build_cmd(
|
|
861
|
+
req.backend, req.model_path, req.port,
|
|
862
|
+
req.options, req.extra_args,
|
|
863
|
+
binary=configured_binary,
|
|
864
|
+
spec_tokens=None,
|
|
865
|
+
)
|
|
866
|
+
except ValueError:
|
|
867
|
+
# A failure to rebuild the non-spec command simply disables the
|
|
868
|
+
# fallback; it must never block the primary start.
|
|
869
|
+
fallback_cmd = None
|
|
870
|
+
|
|
702
871
|
proc_id = uuid.uuid4().hex[:8]
|
|
703
872
|
proc = ManagedProcess(
|
|
704
873
|
id=proc_id,
|
|
@@ -708,9 +877,16 @@ async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
|
|
|
708
877
|
port=req.port,
|
|
709
878
|
options=req.options,
|
|
710
879
|
extra_args=req.extra_args,
|
|
880
|
+
draft_model_path=req.draft_model_path,
|
|
881
|
+
mtp_mode=req.mtp_mode,
|
|
711
882
|
status="starting",
|
|
883
|
+
spec_tokens=spec_tokens,
|
|
884
|
+
spec_auto=spec_auto and fallback_cmd is not None,
|
|
885
|
+
fallback_cmd=fallback_cmd,
|
|
712
886
|
)
|
|
713
887
|
proc.log_tail.append(f"[launcher] cmd: {' '.join(cmd)}")
|
|
888
|
+
for note in spec_notes:
|
|
889
|
+
proc.log_tail.append(f"[launcher] {note}")
|
|
714
890
|
|
|
715
891
|
try:
|
|
716
892
|
p = await asyncio.create_subprocess_exec(
|
|
@@ -732,6 +908,7 @@ async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
|
|
|
732
908
|
proc._proc = p
|
|
733
909
|
proc.pid = p.pid
|
|
734
910
|
proc.status = "running"
|
|
911
|
+
proc.started_at = time.monotonic()
|
|
735
912
|
proc.log_tail.append(f"[launcher] started PID {p.pid}")
|
|
736
913
|
|
|
737
914
|
_registry(request).add(proc)
|
|
@@ -767,6 +944,7 @@ async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
|
|
|
767
944
|
"pid": p.pid,
|
|
768
945
|
"command": cmd,
|
|
769
946
|
"provider_sync": provider_sync,
|
|
947
|
+
"speculative": spec_tokens,
|
|
770
948
|
}
|
|
771
949
|
|
|
772
950
|
|
|
@@ -971,6 +1149,20 @@ _LAUNCHER_HTML = r"""<!doctype html>
|
|
|
971
1149
|
<div id="profile-args" class="mt-1 text-xs font-mono text-slate-400 bg-slate-800/50 rounded p-2 hidden"></div>
|
|
972
1150
|
</div>
|
|
973
1151
|
|
|
1152
|
+
<div class="grid grid-cols-2 gap-2">
|
|
1153
|
+
<div>
|
|
1154
|
+
<label class="block text-xs text-slate-400 mb-1">MTP/draft gguf (空欄で自動検出)</label>
|
|
1155
|
+
<input id="f-draft" type="text" placeholder="companion .gguf (任意)" />
|
|
1156
|
+
</div>
|
|
1157
|
+
<div>
|
|
1158
|
+
<label class="block text-xs text-slate-400 mb-1">MTP</label>
|
|
1159
|
+
<select id="f-mtp">
|
|
1160
|
+
<option value="auto">auto</option>
|
|
1161
|
+
<option value="off">off</option>
|
|
1162
|
+
</select>
|
|
1163
|
+
</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
|
|
974
1166
|
<div>
|
|
975
1167
|
<div class="flex items-center justify-between mb-1">
|
|
976
1168
|
<label class="block text-xs text-slate-400">追加オプション(自由入力)</label>
|
|
@@ -1292,6 +1484,8 @@ _LAUNCHER_HTML = r"""<!doctype html>
|
|
|
1292
1484
|
const backend = document.getElementById("f-backend").value;
|
|
1293
1485
|
const model = document.getElementById("f-model").value.trim();
|
|
1294
1486
|
const extra = document.getElementById("f-extra").value.trim();
|
|
1487
|
+
const draft = document.getElementById("f-draft").value.trim();
|
|
1488
|
+
const mtp = document.getElementById("f-mtp").value;
|
|
1295
1489
|
|
|
1296
1490
|
if (!name) { showLaunchErr("名前を入力してください"); return; }
|
|
1297
1491
|
if (!model) { showLaunchErr("モデルパスを入力してください"); return; }
|
|
@@ -1306,7 +1500,8 @@ _LAUNCHER_HTML = r"""<!doctype html>
|
|
|
1306
1500
|
method: "POST",
|
|
1307
1501
|
headers: authHeaders({"Content-Type": "application/json"}),
|
|
1308
1502
|
body: JSON.stringify({name, backend, model_path: model, port,
|
|
1309
|
-
options: selectedProfileArgs(), extra_args: extra
|
|
1503
|
+
options: selectedProfileArgs(), extra_args: extra,
|
|
1504
|
+
draft_model_path: draft || null, mtp_mode: mtp}),
|
|
1310
1505
|
});
|
|
1311
1506
|
const d = await res.json();
|
|
1312
1507
|
if (!res.ok) { showLaunchErr(d.detail || "起動失敗"); return; }
|