coderouter-cli 2.11.0__tar.gz → 2.11.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/.gitignore +5 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/CHANGELOG.md +279 -1
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/PKG-INFO +1 -1
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/adapters/anthropic_native.py +23 -5
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/cli.py +49 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/config/loader.py +65 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/config/schemas.py +85 -3
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/doctor.py +98 -19
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/doctor_apply.py +72 -3
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/gguf_introspect.py +132 -25
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/drift_detection.py +13 -1
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/launcher_routes.py +3 -3
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/launcher_devices.py +21 -9
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/routing/fallback.py +14 -2
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/translation/anthropic.py +14 -1
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/translation/convert.py +7 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/translation/tool_repair.py +193 -76
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/vscode_init.py +172 -16
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/launcher-multi-build.md +17 -2
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/vscode.md +26 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.llamacpp-vllm.yaml +105 -10
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/pyproject.toml +1 -1
- coderouter_cli-2.11.2/tests/test_config_cwd_warning.py +268 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_doctor.py +278 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_doctor_apply.py +281 -0
- coderouter_cli-2.11.2/tests/test_examples_yaml_roundtrip.py +150 -0
- coderouter_cli-2.11.2/tests/test_gguf_introspect.py +292 -0
- coderouter_cli-2.11.2/tests/test_import_hygiene.py +118 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_backend_variants.py +35 -9
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_devices_routes.py +23 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_gui_devices.py +4 -2
- coderouter_cli-2.11.2/tests/test_launcher_gui_events.py +498 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_gui_readiness.py +17 -6
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_variant_gui.py +4 -2
- coderouter_cli-2.11.2/tests/test_stop_reason_forward_compat.py +179 -0
- coderouter_cli-2.11.2/tests/test_tool_repair_differential.py +326 -0
- coderouter_cli-2.11.2/tests/test_tool_repair_scanners.py +169 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_vscode_init.py +180 -0
- coderouter_cli-2.11.0/tests/test_gguf_introspect.py +0 -164
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/LICENSE +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/README.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/README.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/adapters/base.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/adapters/openai_compat.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/cost.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/errors.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/context_budget.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/self_healing.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/anthropic_routes.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/openai_routes.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/launcher_speculative.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/launcher_swap.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/logging.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/routing/capability.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/state/audit_log.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/state/request_log.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/README.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/README.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/external-agents.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/external-agents.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/gguf_dl.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/hf-ollama-models.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/launcher-quickstart.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/launcher.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/launcher.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/llamacpp-direct.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/llamacpp-direct.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/architecture.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/context-budget.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/context-budget.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/continuous-probing.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/drift-detection.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/partial-stitch.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/agent-cli-plugin-extraction.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/external-agents-adapter.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/launcher-model-swap.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/orchestration-companion.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/remote-access.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/remote-access.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/security.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/subagent-routing.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/subagent-routing.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/low-memory-integration.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/.env.example +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/README.md +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers-agent-cli.yaml +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers-multiagent.yaml +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.raspberrypi.yaml +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.swap.yaml +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.yaml +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/__init__.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/conftest.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_agent_cli.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_auto_router.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_budget.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_capability.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_capability_tool_choice.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_cli.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_cli_bind_warning.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_config.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_config_shim_actions.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_context_budget.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_errors.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_cache_strip.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_empty_response.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_tool_choice_emulate.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_h1_prometheus_drift.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_h2_h6_translation.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_h3_client_reuse.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_h4_h5_guards.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_h7_probe_restore.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_h8_launcher_auth.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_mA_routing.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_mB_adapters.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_mC_convert.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_mD_logging_metrics.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_mE_config_validation.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_mF_ingress.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_ingress_count_tokens.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_config_bench.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_devices.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_devices_detect.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_mtp.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_mtp_fallback.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_provider_sync.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_readiness_restart.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_swap.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_swap_review.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_variant_config.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_variant_routes.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_variant_sweep.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_models_passthrough.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_plugin_adapter.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_tool_repair_lenient.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_tool_repair_r4.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_toolrepair_bench.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_translation_reverse.py +0 -0
|
@@ -9,6 +9,11 @@ secrets.env
|
|
|
9
9
|
~/.coderouter/
|
|
10
10
|
CLAUDE.md
|
|
11
11
|
|
|
12
|
+
# *.bak — pre-overwrite backups of the files above (e.g. `vscode-init --force`
|
|
13
|
+
# saves `.envrc.bak` before clobbering `.envrc`; `doctor --apply` saves
|
|
14
|
+
# `providers.yaml.bak`). Same secret-risk as the originals, so same rule.
|
|
15
|
+
*.bak
|
|
16
|
+
|
|
12
17
|
# ============================================================
|
|
13
18
|
# Python
|
|
14
19
|
# ============================================================
|
|
@@ -9,6 +9,280 @@ are kept verbatim where the Japanese text itself is the subject).
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## [v2.11.2] — 2026-08-04 (file-write safety and launcher event hygiene)
|
|
13
|
+
|
|
14
|
+
Two fixes from the same review round as v2.11.1, split out because they
|
|
15
|
+
rewrite `_atomic_write` and the launcher GUI's event plumbing and deserved
|
|
16
|
+
their own rollback unit. **No breaking changes**: no config-schema field
|
|
17
|
+
changed, no CLI flag changed meaning, `--force` still replaces `.envrc`.
|
|
18
|
+
Existing `.vscode/settings.json` merge semantics are unchanged.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **`vscode-init --force` destroyed `.envrc` with no way back.** The
|
|
23
|
+
command replaced the file outright — no merge, no backup — so a
|
|
24
|
+
hand-added `source_env_if_exists .envrc.local`, the very pattern
|
|
25
|
+
`docs/guides/vscode.md` recommends, vanished silently. direnv then
|
|
26
|
+
looked healthy while exporting nothing, which is a hard failure to
|
|
27
|
+
trace back to its cause. The conflict message said only "Re-run with
|
|
28
|
+
`--force` to overwrite" and never mentioned that the whole file goes.
|
|
29
|
+
The previous contents are now copied to `.envrc.bak` before the write,
|
|
30
|
+
the path is reported in the command's output, and the conflict message
|
|
31
|
+
spells out that this is a whole-file replacement rather than a merge.
|
|
32
|
+
`--dry-run` makes no backup. Only one generation is kept, so a second
|
|
33
|
+
`--force` overwrites the first `.bak` (`vscode_init.py`).
|
|
34
|
+
- **The launcher GUI could lose track of a running backend because of
|
|
35
|
+
that backend's own log output.** `_log_queue` carried control events
|
|
36
|
+
and raw child stdout in the same channel, with the drain loop
|
|
37
|
+
dispatching on `line.startswith("_ERR_:")` and friends. A child line
|
|
38
|
+
beginning `_ERR_:` ran `del self.processes[proc_id]`, so a live
|
|
39
|
+
llama-server dropped out of the process table — unstoppable from the
|
|
40
|
+
UI, missed by the shutdown handler, holding its port and VRAM until
|
|
41
|
+
killed by hand. A line beginning `_SPAWNED_:` with too few colons
|
|
42
|
+
raised `ValueError` inside the drain loop and discarded that tick's
|
|
43
|
+
log processing entirely. This is reachable in ordinary use: llama.cpp
|
|
44
|
+
echoes GGUF metadata such as the chat template verbatim at startup,
|
|
45
|
+
and `extra_args` wrappers put arbitrary text on the same stream.
|
|
46
|
+
Events are now `(kind, proc_id, payload)` with `kind` in
|
|
47
|
+
`log / spawned / ready / error / devices`; raw child output is
|
|
48
|
+
enqueued unconditionally as `log` and never inspected for markers.
|
|
49
|
+
`_cr_log_queue` got the same treatment, the `"_DEVICES_"` proc_id
|
|
50
|
+
sentinel is gone, and a malformed control payload now skips its own
|
|
51
|
+
event instead of killing the loop. While here, `_SPAWNED_` parsing
|
|
52
|
+
moved from `split(":", 2)` to `rpartition`, so model names containing
|
|
53
|
+
a colon no longer mangle the port (`launcher_gui.py`).
|
|
54
|
+
|
|
55
|
+
### Security
|
|
56
|
+
|
|
57
|
+
- **`vscode-init` widened the permissions of the files it rewrote.**
|
|
58
|
+
`_atomic_write` created its temp file under the process umask and
|
|
59
|
+
`os.replace`d it over the target, so an `.envrc` hardened to `0600` —
|
|
60
|
+
a file that holds `ANTHROPIC_AUTH_TOKEN` — came back `0644`. The temp
|
|
61
|
+
path was also fully predictable (`.envrc.tmp`, `settings.json.tmp`),
|
|
62
|
+
so a pre-planted symlink at that path was followed and its target
|
|
63
|
+
overwritten (CWE-377), and two concurrent runs in one workspace raced
|
|
64
|
+
on the same name. Writes now go through `tempfile.mkstemp` with
|
|
65
|
+
`O_EXCL`, are `fsync`ed, restore the original mode before the rename,
|
|
66
|
+
and clean up the temp file on failure. A newly created `.envrc` is
|
|
67
|
+
written `0600`, matching the standard `coderouter doctor --check-env`
|
|
68
|
+
already enforces for credential-bearing files; an existing file keeps
|
|
69
|
+
whatever mode the operator set. Mode handling is POSIX-only —
|
|
70
|
+
`os.chmod` on Windows only toggles the read-only bit and is skipped.
|
|
71
|
+
`.vscode/settings.json` is backed up before any rewrite too, not just
|
|
72
|
+
under `--force`, since the merge path also edits it in place
|
|
73
|
+
(`vscode_init.py`).
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- `.gitignore` now excludes `*.bak`, so the backups above and
|
|
78
|
+
`providers.yaml.bak` from `doctor --apply` stay out of commits. They
|
|
79
|
+
carry the same secrets as the files they shadow.
|
|
80
|
+
- Three GUI test modules documented their `pytest.importorskip("tkinter")`
|
|
81
|
+
as a CI workaround, stating that the uv-managed Python on CI has no
|
|
82
|
+
tkinter and that these tests are therefore skipped there. Measured on
|
|
83
|
+
2026-08-04: `uv python install` ships Tk 8.6 on both 3.12.11 and
|
|
84
|
+
3.13.7, and all 93 cases in those files run and pass on CI. The
|
|
85
|
+
docstrings now say so, and record that the guards stay for local
|
|
86
|
+
system pythons built without Tk — which do exist.
|
|
87
|
+
|
|
88
|
+
### Tests
|
|
89
|
+
|
|
90
|
+
- 1 new file, 38 new cases (`test_launcher_gui_events.py`, plus
|
|
91
|
+
additions to `test_vscode_init.py`). Suite: 2345 passed, 1 skipped,
|
|
92
|
+
ruff clean.
|
|
93
|
+
- Each fix has a regression test confirmed to fail against the pre-fix
|
|
94
|
+
tree — 10 for the write path, 7 for the event path. The headline one
|
|
95
|
+
asserts a live process survives its own `_ERR_:` stdout line; on the
|
|
96
|
+
old code `app.processes` comes back empty. Another plants a symlink at
|
|
97
|
+
the predictable temp path and asserts its target is untouched.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## [v2.11.1] — 2026-08-04 (second-round review: correctness & hardening)
|
|
102
|
+
|
|
103
|
+
Seven fixes from a full-source review of the 36k-line package, plus two
|
|
104
|
+
deprecation warnings that pre-announce the behaviour changes queued for
|
|
105
|
+
v2.12.0. **No breaking changes**: no config-schema field was removed or
|
|
106
|
+
narrowed, no CLI flag changed meaning, and every default is unchanged.
|
|
107
|
+
The two new warnings do not alter behaviour — they only tell you which
|
|
108
|
+
of your settings will need editing before v2.12.0.
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
111
|
+
|
|
112
|
+
- **Brace scanning in `tool_repair` was quadratic and blocked the event
|
|
113
|
+
loop for minutes.** `_find_balanced_json_objects` and
|
|
114
|
+
`_find_candidate_object_spans` restarted a full scan to end-of-text at
|
|
115
|
+
every `{`, so an assistant reply carrying unclosed braces — the
|
|
116
|
+
ordinary shape of a deep nested JSON truncated by `max_tokens` — cost
|
|
117
|
+
O(k·n). Measured on the old code: 3.997 s for 8 KiB of `{`, 22.199 s
|
|
118
|
+
for a 48 KiB reply, 139.019 s for 48 KiB of bare `{`. Both scanners
|
|
119
|
+
are called synchronously from `to_anthropic_response`, which
|
|
120
|
+
`FallbackEngine._generate_anthropic_impl` / `._stream_anthropic_impl`
|
|
121
|
+
invoke from inside `async` handlers, so the whole server stalled for
|
|
122
|
+
the duration — every concurrent request, not just the offending one.
|
|
123
|
+
Replaced with a single right-to-left sweep that builds a depth-0
|
|
124
|
+
closing-brace table (`table[j]` = the first depth-0 `}` reached when
|
|
125
|
+
scanning from offset `j` outside any string); the answer for an
|
|
126
|
+
opening brace at `q` is `table[q+1]`, which is by definition the
|
|
127
|
+
fresh-restart semantics the old code had. Same inputs now take
|
|
128
|
+
0.004 s / 0.016 s / 0.024 s. The strict (`"` only) and lenient (`'`
|
|
129
|
+
too) quoting rules live in separate tables and cannot bleed into each
|
|
130
|
+
other. Scan output is unchanged — see Tests. Added
|
|
131
|
+
`_MAX_BARE_SCAN_CHARS` (256 KiB); above it the bare-JSON pass is
|
|
132
|
+
skipped with a `tool-repair-input-too-large` warning while the fenced
|
|
133
|
+
/ XML / R4c passes still run. The two `to_anthropic_response` call
|
|
134
|
+
sites moved to `asyncio.to_thread`; the function itself stays
|
|
135
|
+
synchronous (`tool_repair.py`, `routing/fallback.py`).
|
|
136
|
+
- **`doctor --apply` dropped `output_filters` a user had configured.**
|
|
137
|
+
The probe emitted only the filters that were *missing*, but
|
|
138
|
+
`deep_merge_dicts` replaces lists wholesale, so applying
|
|
139
|
+
`output_filters: [strip_thinking]` to a provider already carrying
|
|
140
|
+
`[strip_stop_markers, my_filter]` left it with `[strip_thinking]`
|
|
141
|
+
alone. `strip_tool_call_xml` and `repair_byte_fallback` were dropped
|
|
142
|
+
100% of the time, since no probe ever recommends them. The patch now
|
|
143
|
+
carries the union — existing chain first, missing filters appended.
|
|
144
|
+
Order is load-bearing (`OutputFilterChain.feed` applies left to
|
|
145
|
+
right), so the union never sorts; `deep_merge_dicts` semantics are
|
|
146
|
+
untouched (`doctor.py`).
|
|
147
|
+
- **`doctor --check-model` could write `capabilities: tools: false` for
|
|
148
|
+
a model that supports tools.** The tool-call probe judged on
|
|
149
|
+
`content[:200]` — a display-oriented truncation — while the probe
|
|
150
|
+
budget is 256 tokens, or 1024 for thinking models (roughly 3–4 k
|
|
151
|
+
chars). A model that emits a preamble before its tool JSON, which is
|
|
152
|
+
exactly what thinking models do, fell through to the "nothing
|
|
153
|
+
tool-shaped at all" branch and got a `tools: false` patch that
|
|
154
|
+
`--apply` then wrote. This re-created, by a different route, the
|
|
155
|
+
false positive the v1.8.3 budget increase was meant to remove.
|
|
156
|
+
Detection now reads the full body up to `_TOOL_PROBE_SCAN_CHARS`
|
|
157
|
+
(16 KiB) and the 200-char slice is display-only; the detail line
|
|
158
|
+
reports how many chars were actually scanned (`doctor.py`).
|
|
159
|
+
- **Stray `stop_reason` values from Anthropic were treated as malformed
|
|
160
|
+
responses.** `AnthropicResponse.stop_reason` was a closed `Literal`,
|
|
161
|
+
and `ConfigDict(extra="allow")` does not widen a declared field, so
|
|
162
|
+
`pause_turn` (server-tool turns), `refusal` and
|
|
163
|
+
`model_context_window_exceeded` raised `ValidationError`.
|
|
164
|
+
`AnthropicAdapter` converts that to a retryable `AdapterError`, so a
|
|
165
|
+
perfectly good — and already billed — reply was discarded and the
|
|
166
|
+
chain fell through to the next provider, or to
|
|
167
|
+
`NoProvidersAvailableError` if it was the last. Streaming was
|
|
168
|
+
unaffected (`AnthropicStreamEvent` keeps its payload as a plain
|
|
169
|
+
dict), so only non-streaming requests were hit. The field is now
|
|
170
|
+
`str | None`, deliberately open: Anthropic has added stop reasons
|
|
171
|
+
three times and a closed `Literal` reproduces this bug on the next
|
|
172
|
+
one. `_REVERSE_FINISH_REASON_MAP` gained `pause_turn` → `stop`,
|
|
173
|
+
`refusal` → `content_filter`, `model_context_window_exceeded` →
|
|
174
|
+
`length`. While here, `drift_detection._EXPECTED_STOP` gained
|
|
175
|
+
`stop_sequence` — it was counting a documented, ordinary stop reason
|
|
176
|
+
as an anomaly (`translation/anthropic.py`, `translation/convert.py`,
|
|
177
|
+
`guards/drift_detection.py`).
|
|
178
|
+
- **`import coderouter.translation` failed on its own.** The package
|
|
179
|
+
reached `adapters` → `registry` → `anthropic_native`, which imported
|
|
180
|
+
`translation.convert` back at module scope. `pytest tests/` passed
|
|
181
|
+
only because alphabetical collection imports `test_adapter_anthropic`
|
|
182
|
+
first and primes `coderouter.adapters`; running
|
|
183
|
+
`pytest tests/test_tool_repair.py` on its own was a collection error.
|
|
184
|
+
The three `convert` symbols moved into the `generate()` / `stream()`
|
|
185
|
+
bodies that use them (`adapters/anthropic_native.py`).
|
|
186
|
+
|
|
187
|
+
### Security
|
|
188
|
+
|
|
189
|
+
- **`doctor --apply` silently rewrote `providers.yaml` while reporting
|
|
190
|
+
"already up to date", then destroyed the original in `.bak`.** The
|
|
191
|
+
write was gated on `diff_text` — "does a ruamel re-dump differ from
|
|
192
|
+
what is on disk" — not on whether a patch changed anything. ruamel
|
|
193
|
+
re-flows quoted scalars at 80 columns and renders an explicit `null`
|
|
194
|
+
as an empty scalar, so a fully idempotent re-run still produced a
|
|
195
|
+
diff and still wrote. The CLI meanwhile took the `is_no_op` early
|
|
196
|
+
return and printed `All N patch(es) already applied`, showing neither
|
|
197
|
+
the diff nor a backup line. The *next* `--apply` then copied that
|
|
198
|
+
already-reformatted text over `providers.yaml.bak`, which is
|
|
199
|
+
deliberately un-timestamped and single-slot — so the only copy of the
|
|
200
|
+
operator's original was gone. The shipped `examples/providers.yaml`
|
|
201
|
+
is affected (five `api_key_env: null` lines), as is
|
|
202
|
+
`examples/providers.llamacpp-vllm.yaml` (two `binary: null`). The
|
|
203
|
+
write is now gated on the merge result per target file; a target
|
|
204
|
+
whose merges were all no-ops reports an empty diff and parks the
|
|
205
|
+
cosmetic delta in `ApplyResult.reformat_only`, surfaced under
|
|
206
|
+
`--dry-run` only. The dumper also sets `width = 4096`, as defence in
|
|
207
|
+
depth. Every exit path of the apply command — including both
|
|
208
|
+
exception returns — now states how many files were written.
|
|
209
|
+
**If you ran `doctor --apply` on v2.11.0 or earlier, compare
|
|
210
|
+
`providers.yaml` against `providers.yaml.bak` once.** The
|
|
211
|
+
reformatting is semantically equivalent YAML, but the first `--apply`
|
|
212
|
+
after upgrading will show the un-wrapping alongside your intended
|
|
213
|
+
change; that is expected and happens once (`doctor_apply.py`,
|
|
214
|
+
`cli.py`).
|
|
215
|
+
- **A malformed GGUF file could hang or crash the launcher, including
|
|
216
|
+
from the HTTP route.** `_skip_value` recursed once per array nesting
|
|
217
|
+
level at 12 bytes per level, so a ~12 KB file raised `RecursionError`
|
|
218
|
+
— which is not `GGUFParseError`, so it went straight through
|
|
219
|
+
`try_read_gguf_metadata`'s handler. Separately, array elements were
|
|
220
|
+
skipped with `fh.seek(size, 1)` per element with no end-of-file
|
|
221
|
+
check: a 49-byte file declaring `count = 1<<24` spun 16.7 M times,
|
|
222
|
+
took 7.94 s, and returned a `GGUFInfo` rather than raising. Both are
|
|
223
|
+
reachable from `POST /api/launcher/start`, and
|
|
224
|
+
`launcher_speculative.find_draft_companion` reads *every* `.gguf` in
|
|
225
|
+
the configured model directories, so one bad file poisons the scan.
|
|
226
|
+
`_skip_value` is now iterative over an explicit work stack —
|
|
227
|
+
`RecursionError` is structurally unreachable — with
|
|
228
|
+
`_MAX_ARRAY_DEPTH = 8` retained as an independent bound (real GGUF
|
|
229
|
+
arrays are depth 1; llama.cpp's writer emits nothing deeper). Every
|
|
230
|
+
skip validates against the file size before seeking, and fixed-width
|
|
231
|
+
scalar arrays are skipped in one bulk seek: a 150 k-element `u32`
|
|
232
|
+
array went from 0.0624 s to 0.000102 s, and the 49-byte case from
|
|
233
|
+
7.94 s to 0.000043 s with a `GGUFParseError`. `p.open()` is now
|
|
234
|
+
wrapped for the stat/open TOCTOU, and `try_read_gguf_metadata`
|
|
235
|
+
catches `OSError` / `RecursionError` / `MemoryError` as well
|
|
236
|
+
(`gguf_introspect.py`).
|
|
237
|
+
|
|
238
|
+
### Changed
|
|
239
|
+
|
|
240
|
+
- **Deprecation warning: `providers.yaml` discovered in the current
|
|
241
|
+
directory.** When neither `--config` nor `CODEROUTER_CONFIG` resolves
|
|
242
|
+
and the file actually loaded came from `./providers.yaml`, a
|
|
243
|
+
`cwd-config-loaded` warning is logged once per process naming the
|
|
244
|
+
path. In v2.12.0 this search step becomes opt-in behind
|
|
245
|
+
`CODEROUTER_ALLOW_CWD_CONFIG`, because starting the server in an
|
|
246
|
+
untrusted directory currently means loading that directory's
|
|
247
|
+
execution-bearing settings. Behaviour is unchanged in this release
|
|
248
|
+
(`config/loader.py`).
|
|
249
|
+
- **Deprecation warning: shell syntax in `restart_command`.** A value
|
|
250
|
+
containing `&&`, `||`, `|`, `;`, `>`, `<`, `$(` or a backtick, or
|
|
251
|
+
starting with `~/` or `FOO=bar`, now logs
|
|
252
|
+
`restart-command-shell-syntax` at config load. In v2.12.0 the command
|
|
253
|
+
will be `shlex.split` and run with `shell=False`, so those forms
|
|
254
|
+
break — and `pkill x && x` breaks *silently*, with `&&` passed to
|
|
255
|
+
`pkill` as an argument. Wrap such commands in a script, or use an
|
|
256
|
+
explicit `/bin/sh -c "..."`. This is a warning only; nothing is
|
|
257
|
+
rejected and execution is unchanged (`config/schemas.py`).
|
|
258
|
+
|
|
259
|
+
### Tests
|
|
260
|
+
|
|
261
|
+
- 6 new files, 184 new cases (`test_tool_repair_differential.py`,
|
|
262
|
+
`test_tool_repair_scanners.py`, `test_examples_yaml_roundtrip.py`,
|
|
263
|
+
`test_import_hygiene.py`, `test_stop_reason_forward_compat.py`,
|
|
264
|
+
`test_config_cwd_warning.py`, plus additions to `test_doctor.py`,
|
|
265
|
+
`test_doctor_apply.py` and `test_gguf_introspect.py`). Suite: 2307
|
|
266
|
+
passed, 1 skipped, ruff clean.
|
|
267
|
+
- The scanner rewrite is pinned by a differential test that carries the
|
|
268
|
+
old quadratic algorithm as a reference implementation and compares
|
|
269
|
+
both scanners across five random alphabets × 2500 inputs each, plus
|
|
270
|
+
the full `benchmarks/tool-repair` corpus end to end. A naive global
|
|
271
|
+
single-pass rewrite — the obvious way to linearise this, and the way
|
|
272
|
+
that looks correct — diverges on 6870 of 12500 strict cases and 6977
|
|
273
|
+
lenient, because an apostrophe in ordinary prose (`I'll`, `don't`)
|
|
274
|
+
opens a single-quoted string that swallows every following brace. The
|
|
275
|
+
differential test was verified to catch exactly that.
|
|
276
|
+
- Each fix above has a regression test confirmed to fail against the
|
|
277
|
+
pre-fix tree, including
|
|
278
|
+
`test_cli_no_op_message_states_nothing_written` (the "up to date"
|
|
279
|
+
message printed while writing),
|
|
280
|
+
`test_no_op_apply_leaves_example_byte_identical` (the shipped
|
|
281
|
+
examples), `test_array_count_past_eof_raises_immediately` (raises,
|
|
282
|
+
and inside 0.5 s) and `test_lenient_survives_prose_apostrophe`.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
12
286
|
## [v2.11.0] — 2026-07-28 (launcher backend variants)
|
|
13
287
|
|
|
14
288
|
### Added
|
|
@@ -52,7 +326,11 @@ are kept verbatim where the Japanese text itself is the subject).
|
|
|
52
326
|
exist in the chosen build with a 400 instead of letting
|
|
53
327
|
`--device CUDA0` reach a Vulkan build and fail at startup. Skipped
|
|
54
328
|
when `--list-devices` itself fails (best-effort, per the
|
|
55
|
-
`hardware.py` invariant). New `launcher_devices.
|
|
329
|
+
`hardware.py` invariant). New `launcher_devices.foreign_device_ids`,
|
|
330
|
+
which compares the backend *prefix* (`backend_of`) rather than the
|
|
331
|
+
exact id — the mismatch worth catching is the wrong build, not a
|
|
332
|
+
number gap, and prefix matching stays correct if a future
|
|
333
|
+
`--list-devices` format change makes `parse_list_devices` miss a line.
|
|
56
334
|
|
|
57
335
|
### Changed
|
|
58
336
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.11.
|
|
3
|
+
Version: 2.11.2
|
|
4
4
|
Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
|
|
5
5
|
Project-URL: Homepage, https://github.com/zephel01/CodeRouter
|
|
6
6
|
Project-URL: Repository, https://github.com/zephel01/CodeRouter
|
|
@@ -53,11 +53,16 @@ from coderouter.translation.anthropic import (
|
|
|
53
53
|
AnthropicResponse,
|
|
54
54
|
AnthropicStreamEvent,
|
|
55
55
|
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
56
|
+
|
|
57
|
+
# NOTE: `coderouter.translation.convert` is intentionally NOT imported here
|
|
58
|
+
# at module scope (see H-7). The import chain is:
|
|
59
|
+
# translation/__init__.py -> convert.py -> adapters/__init__.py
|
|
60
|
+
# -> registry.py -> anthropic_native.py -> translation.convert (again)
|
|
61
|
+
# A top-level import here closes that cycle and makes standalone imports of
|
|
62
|
+
# `coderouter.translation.*` fail with a partial-initialization ImportError
|
|
63
|
+
# unless `coderouter.adapters` happened to be imported first. The three
|
|
64
|
+
# symbols used from `convert` are imported lazily inside `generate()` /
|
|
65
|
+
# `stream()` instead, which breaks the cycle without changing behavior.
|
|
61
66
|
|
|
62
67
|
logger = get_logger(__name__)
|
|
63
68
|
|
|
@@ -210,6 +215,12 @@ class AnthropicAdapter(BaseAdapter):
|
|
|
210
215
|
``to_chat_response``. The ``coderouter_provider`` tag is preserved
|
|
211
216
|
on both sides.
|
|
212
217
|
"""
|
|
218
|
+
# Local import to avoid a circular import at module load time (H-7):
|
|
219
|
+
# convert.py transitively imports coderouter.adapters, which imports
|
|
220
|
+
# this module. Importing convert.py here (call time, not load time)
|
|
221
|
+
# keeps `coderouter.translation.*` importable standalone.
|
|
222
|
+
from coderouter.translation.convert import to_anthropic_request, to_chat_response
|
|
223
|
+
|
|
213
224
|
anth_req = to_anthropic_request(request)
|
|
214
225
|
anth_resp = await self.generate_anthropic(anth_req, overrides=overrides)
|
|
215
226
|
chat_resp = to_chat_response(anth_resp)
|
|
@@ -241,6 +252,13 @@ class AnthropicAdapter(BaseAdapter):
|
|
|
241
252
|
- Anthropic ``event: error`` → translator raises
|
|
242
253
|
AdapterError(retryable=False), same treatment as above.
|
|
243
254
|
"""
|
|
255
|
+
# Local import to avoid a circular import at module load time (H-7);
|
|
256
|
+
# see the matching comment in generate() above.
|
|
257
|
+
from coderouter.translation.convert import (
|
|
258
|
+
stream_anthropic_to_chat_chunks,
|
|
259
|
+
to_anthropic_request,
|
|
260
|
+
)
|
|
261
|
+
|
|
244
262
|
anth_req = to_anthropic_request(request)
|
|
245
263
|
events = self.stream_anthropic(anth_req, overrides=overrides)
|
|
246
264
|
async for chunk in stream_anthropic_to_chat_chunks(events, provider_name=self.name):
|
|
@@ -624,6 +624,24 @@ def _resolve_config_path(explicit: str | None) -> Path:
|
|
|
624
624
|
return candidates[-1]
|
|
625
625
|
|
|
626
626
|
|
|
627
|
+
def _write_verdict_line(*, write: bool, files_written: int) -> None:
|
|
628
|
+
"""Print the one-line "did we touch the disk?" verdict.
|
|
629
|
+
|
|
630
|
+
Called on **every** exit path of :func:`_run_apply_or_dry_run`.
|
|
631
|
+
The v1.x regression this guards against: an ``--apply`` run that
|
|
632
|
+
silently rewrote providers.yaml while the summary said "already
|
|
633
|
+
applied" and printed neither a diff nor a ``Backup:`` line. Making
|
|
634
|
+
the statement unconditional means no future early-return can go
|
|
635
|
+
quiet about a write again.
|
|
636
|
+
"""
|
|
637
|
+
if not write:
|
|
638
|
+
print(" 0 files written (dry-run).")
|
|
639
|
+
elif files_written == 0:
|
|
640
|
+
print(" 0 files written — no file contents changed.")
|
|
641
|
+
else:
|
|
642
|
+
print(f" {files_written} file(s) written.")
|
|
643
|
+
|
|
644
|
+
|
|
627
645
|
def _run_apply_or_dry_run(
|
|
628
646
|
*,
|
|
629
647
|
report: object,
|
|
@@ -655,9 +673,13 @@ def _run_apply_or_dry_run(
|
|
|
655
673
|
)
|
|
656
674
|
except MissingDependencyError as exc:
|
|
657
675
|
print(f"doctor --apply: {exc}", file=sys.stderr)
|
|
676
|
+
_write_verdict_line(write=write, files_written=0)
|
|
658
677
|
return 1
|
|
659
678
|
except DoctorApplyError as exc:
|
|
679
|
+
# Every merge happens before the first byte is written, so an
|
|
680
|
+
# abort here always leaves the disk untouched.
|
|
660
681
|
print(f"doctor --apply: {exc}", file=sys.stderr)
|
|
682
|
+
_write_verdict_line(write=write, files_written=0)
|
|
661
683
|
return 1
|
|
662
684
|
|
|
663
685
|
label = "Apply" if write else "Dry-run"
|
|
@@ -680,6 +702,8 @@ def _run_apply_or_dry_run(
|
|
|
680
702
|
f" All {result.no_op_patches} patch(es) already applied "
|
|
681
703
|
f"— providers.yaml is up to date."
|
|
682
704
|
)
|
|
705
|
+
_write_verdict_line(write=write, files_written=len(result.written_paths))
|
|
706
|
+
_print_reformat_notice(result, write=write)
|
|
683
707
|
return 0
|
|
684
708
|
|
|
685
709
|
print(
|
|
@@ -694,16 +718,41 @@ def _run_apply_or_dry_run(
|
|
|
694
718
|
print()
|
|
695
719
|
print(diff, end="" if diff.endswith("\n") else "\n")
|
|
696
720
|
|
|
721
|
+
_write_verdict_line(write=write, files_written=len(result.written_paths))
|
|
697
722
|
if write:
|
|
698
723
|
for orig, bak in result.backups.items():
|
|
699
724
|
print(f" Backup: {orig} → {bak}")
|
|
700
725
|
else:
|
|
726
|
+
_print_reformat_notice(result, write=write)
|
|
701
727
|
print()
|
|
702
728
|
print(" (dry-run — no files were modified. Re-run with --apply to write.)")
|
|
703
729
|
|
|
704
730
|
return 0
|
|
705
731
|
|
|
706
732
|
|
|
733
|
+
def _print_reformat_notice(result: object, *, write: bool) -> None:
|
|
734
|
+
"""Dry-run-only heads-up about cosmetic re-serialization deltas.
|
|
735
|
+
|
|
736
|
+
``reformat_only`` holds targets whose re-dump differs from disk
|
|
737
|
+
without any patch having changed a value — today that is explicit
|
|
738
|
+
``key: null`` scalars, which ruamel re-emits as empty scalars.
|
|
739
|
+
These bytes are never written, so the note is purely so an operator
|
|
740
|
+
comparing "the diff I expected" with "the file I have" is not
|
|
741
|
+
surprised. Suppressed under ``--apply``, where nothing about them
|
|
742
|
+
is actionable.
|
|
743
|
+
"""
|
|
744
|
+
reformat = getattr(result, "reformat_only", None) or {}
|
|
745
|
+
if write or not reformat:
|
|
746
|
+
return
|
|
747
|
+
print(
|
|
748
|
+
f" note: {len(reformat)} file(s) would re-serialize with cosmetic "
|
|
749
|
+
"differences (e.g. explicit `null` → empty scalar). These are NOT "
|
|
750
|
+
"written — --apply only rewrites files whose values actually changed."
|
|
751
|
+
)
|
|
752
|
+
for path in sorted(reformat):
|
|
753
|
+
print(f" - {path}")
|
|
754
|
+
|
|
755
|
+
|
|
707
756
|
|
|
708
757
|
|
|
709
758
|
def _run_audit(args: argparse.Namespace) -> int:
|
|
@@ -7,6 +7,17 @@ Search order (first hit wins):
|
|
|
7
7
|
4. ~/.coderouter/providers.yaml
|
|
8
8
|
|
|
9
9
|
Secrets are resolved by reading the env var named by `api_key_env`.
|
|
10
|
+
|
|
11
|
+
.. note::
|
|
12
|
+
**[Unreleased] deprecation notice (planned v2.12.0):** step 3 above
|
|
13
|
+
(implicit CWD discovery) is slated to become opt-in, gated behind
|
|
14
|
+
``CODEROUTER_ALLOW_CWD_CONFIG``. Landing that change without warning
|
|
15
|
+
first would silently break anyone currently relying on "run from the
|
|
16
|
+
directory with providers.yaml in it" with no diagnostic. Until that
|
|
17
|
+
lands, :func:`load_config` emits a one-time ``cwd-config-loaded``
|
|
18
|
+
warning whenever step 3 is the one that actually resolved — never
|
|
19
|
+
when an explicit ``--config`` / ``CODEROUTER_CONFIG`` path was used,
|
|
20
|
+
even if it happens to point at the same file.
|
|
10
21
|
"""
|
|
11
22
|
|
|
12
23
|
from __future__ import annotations
|
|
@@ -17,6 +28,14 @@ from pathlib import Path
|
|
|
17
28
|
import yaml
|
|
18
29
|
|
|
19
30
|
from coderouter.config.schemas import CodeRouterConfig
|
|
31
|
+
from coderouter.logging import get_logger
|
|
32
|
+
|
|
33
|
+
logger = get_logger(__name__)
|
|
34
|
+
|
|
35
|
+
# Guards the one-time [Unreleased]/v2.12.0 CWD-config deprecation warning
|
|
36
|
+
# (see module docstring) so a process that calls load_config() repeatedly
|
|
37
|
+
# (tests, hot-reload, multiple create_app() calls) only logs it once.
|
|
38
|
+
_cwd_config_warning_emitted = False
|
|
20
39
|
|
|
21
40
|
|
|
22
41
|
def _candidate_paths(explicit: str | os.PathLike[str] | None) -> list[Path]:
|
|
@@ -30,6 +49,51 @@ def _candidate_paths(explicit: str | os.PathLike[str] | None) -> list[Path]:
|
|
|
30
49
|
return paths
|
|
31
50
|
|
|
32
51
|
|
|
52
|
+
def _warn_if_cwd_config(
|
|
53
|
+
chosen: Path, *, explicit: str | os.PathLike[str] | None
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Emit the one-time [Unreleased]/v2.12.0 CWD-config deprecation warning.
|
|
56
|
+
|
|
57
|
+
Only fires when ``chosen`` was resolved via the *implicit* CWD-search
|
|
58
|
+
step. Compares ``chosen`` against the *literal* explicit / env
|
|
59
|
+
candidate paths (not merely whether those inputs were set) so that:
|
|
60
|
+
|
|
61
|
+
* an explicit ``--config`` (or ``CODEROUTER_CONFIG``) path that
|
|
62
|
+
happens to coincide with ``./providers.yaml`` stays quiet — an
|
|
63
|
+
explicit choice, however coincidental, is not the thing being
|
|
64
|
+
deprecated; but
|
|
65
|
+
* an explicit path/env var that was set but did NOT resolve (file
|
|
66
|
+
missing, so the search fell through to CWD) still warns, because
|
|
67
|
+
the CWD step is what actually served the config in that case.
|
|
68
|
+
"""
|
|
69
|
+
global _cwd_config_warning_emitted
|
|
70
|
+
if _cwd_config_warning_emitted:
|
|
71
|
+
return
|
|
72
|
+
cwd_path = Path.cwd() / "providers.yaml"
|
|
73
|
+
if chosen != cwd_path:
|
|
74
|
+
return
|
|
75
|
+
if explicit and Path(explicit) == chosen:
|
|
76
|
+
return
|
|
77
|
+
if (env_path := os.environ.get("CODEROUTER_CONFIG")) and Path(env_path) == chosen:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
_cwd_config_warning_emitted = True
|
|
81
|
+
logger.warning(
|
|
82
|
+
"cwd-config-loaded",
|
|
83
|
+
extra={
|
|
84
|
+
"path": str(chosen),
|
|
85
|
+
"hint": (
|
|
86
|
+
f"loaded providers.yaml from the current working "
|
|
87
|
+
f"directory ({chosen}). Starting in v2.12.0, this "
|
|
88
|
+
"implicit CWD discovery becomes opt-in (gated behind "
|
|
89
|
+
"CODEROUTER_ALLOW_CWD_CONFIG) — pass --config or set "
|
|
90
|
+
"CODEROUTER_CONFIG to pin the file explicitly and avoid "
|
|
91
|
+
"a startup break when that lands."
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
33
97
|
def load_config(path: str | os.PathLike[str] | None = None) -> CodeRouterConfig:
|
|
34
98
|
"""Load providers.yaml + apply ALLOW_PAID env override."""
|
|
35
99
|
candidates = _candidate_paths(path)
|
|
@@ -40,6 +104,7 @@ def load_config(path: str | os.PathLike[str] | None = None) -> CodeRouterConfig:
|
|
|
40
104
|
f"providers.yaml not found. Searched:\n {searched}\n"
|
|
41
105
|
f"Hint: copy examples/providers.yaml to ~/.coderouter/providers.yaml"
|
|
42
106
|
)
|
|
107
|
+
_warn_if_cwd_config(chosen, explicit=path)
|
|
43
108
|
|
|
44
109
|
with chosen.open("r", encoding="utf-8") as f:
|
|
45
110
|
raw = yaml.safe_load(f) or {}
|
|
@@ -26,6 +26,10 @@ from pydantic import (
|
|
|
26
26
|
model_validator,
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
+
from coderouter.logging import get_logger
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
|
|
29
33
|
|
|
30
34
|
class Capabilities(BaseModel):
|
|
31
35
|
"""Capability flags per provider (plan.md §2.5)."""
|
|
@@ -350,6 +354,17 @@ class AgentCliConfig(BaseModel):
|
|
|
350
354
|
return self
|
|
351
355
|
|
|
352
356
|
|
|
357
|
+
# [Unreleased]/planned v2.12.0: patterns used by ProviderConfig's
|
|
358
|
+
# ``_warn_restart_command_shell_syntax`` validator to flag
|
|
359
|
+
# ``restart_command`` values that rely on shell syntax not honored by
|
|
360
|
+
# the planned ``shlex.split`` + ``shell=False`` dispatch (see that
|
|
361
|
+
# validator's docstring and the field description below for the full
|
|
362
|
+
# rationale). Compiled once at module scope since every ProviderConfig
|
|
363
|
+
# validation reuses them.
|
|
364
|
+
_RESTART_COMMAND_SHELL_META_RE = re.compile(r"(&&|\|\||\||;|>|<|\$\(|`)")
|
|
365
|
+
_RESTART_COMMAND_ENV_PREFIX_RE = re.compile(r"^\w+=")
|
|
366
|
+
|
|
367
|
+
|
|
353
368
|
class ProviderConfig(BaseModel):
|
|
354
369
|
"""A single provider entry from providers.yaml.
|
|
355
370
|
|
|
@@ -497,15 +512,82 @@ class ProviderConfig(BaseModel):
|
|
|
497
512
|
restart_command: str | None = Field(
|
|
498
513
|
default=None,
|
|
499
514
|
description=(
|
|
500
|
-
"v2.0-J (Self-healing):
|
|
501
|
-
"
|
|
515
|
+
"v2.0-J (Self-healing): command to restart this provider's "
|
|
516
|
+
"backend process. Examples: 'ollama serve', "
|
|
502
517
|
"'open -a LM\\ Studio'. Only executed when the profile's "
|
|
503
518
|
"backend_health_action is 'exclude' and the provider "
|
|
504
519
|
"transitions to UNHEALTHY. Unset = no automatic restart "
|
|
505
|
-
"(recovery probe still runs, waiting for manual restart)."
|
|
520
|
+
"(recovery probe still runs, waiting for manual restart). "
|
|
521
|
+
"\n\n"
|
|
522
|
+
"[Unreleased]/planned v2.12.0: currently run as "
|
|
523
|
+
"``subprocess.run(command, shell=True)`` (coderouter/guards/"
|
|
524
|
+
"self_healing.py), so shell syntax works today — pipelines "
|
|
525
|
+
"(``pkill ollama && ollama serve``), ``~`` expansion, and "
|
|
526
|
+
"leading env-var assignments (``OLLAMA_HOST=0.0.0.0 ollama "
|
|
527
|
+
"serve``) are all honored. A future minor switches this to "
|
|
528
|
+
"``shlex.split(command)`` + ``shell=False`` (argv dispatch, "
|
|
529
|
+
"no shell in between), which does NOT support any of the "
|
|
530
|
+
"above — ``&&`` becomes a literal argument to the first "
|
|
531
|
+
"command instead of a separator, ``~/...`` paths are not "
|
|
532
|
+
"expanded, and ``FOO=bar cmd`` fails to exec. If your "
|
|
533
|
+
"command needs shell features, wrap it in a small script "
|
|
534
|
+
"and point restart_command at that script instead, e.g. "
|
|
535
|
+
"``restart_command: /path/to/restart-ollama.sh``."
|
|
506
536
|
),
|
|
507
537
|
)
|
|
508
538
|
|
|
539
|
+
@model_validator(mode="after")
|
|
540
|
+
def _warn_restart_command_shell_syntax(self) -> ProviderConfig:
|
|
541
|
+
"""[Unreleased]/planned v2.12.0 breaking-change heads-up (warning only).
|
|
542
|
+
|
|
543
|
+
``coderouter/guards/self_healing.py`` currently runs
|
|
544
|
+
``restart_command`` via ``subprocess.run(command, shell=True)``.
|
|
545
|
+
The planned v2.12.0 change switches that to
|
|
546
|
+
``shlex.split(command)`` + ``shell=False`` — a shell-syntax
|
|
547
|
+
``restart_command`` that works fine today would then either
|
|
548
|
+
crash (``FileNotFoundError`` for ``~/...`` paths or an
|
|
549
|
+
``ENV=val cmd`` prefix) or, worse, silently misbehave (``&&`` /
|
|
550
|
+
``||`` / ``|`` / ``;`` / redirects becoming literal argv tokens
|
|
551
|
+
instead of shell operators — a provider "restart" that quietly
|
|
552
|
+
does the wrong thing is worse than one that errors loudly).
|
|
553
|
+
|
|
554
|
+
Deliberately never raises: this validator does not enforce
|
|
555
|
+
anything, it only warns ahead of the wire change so operators
|
|
556
|
+
can migrate ``restart_command`` to a plain argv (or a wrapper
|
|
557
|
+
script) before v2.12.0 lands, instead of discovering the break
|
|
558
|
+
the next time a provider actually goes UNHEALTHY.
|
|
559
|
+
"""
|
|
560
|
+
cmd = self.restart_command
|
|
561
|
+
if not cmd:
|
|
562
|
+
return self
|
|
563
|
+
stripped = cmd.strip()
|
|
564
|
+
reasons: list[str] = []
|
|
565
|
+
if _RESTART_COMMAND_SHELL_META_RE.search(cmd):
|
|
566
|
+
reasons.append("shell metacharacter (&&, ||, |, ;, >, <, $(...), or `...`)")
|
|
567
|
+
if stripped.startswith("~/"):
|
|
568
|
+
reasons.append("leading '~/' path (no shell expansion under shell=False)")
|
|
569
|
+
if _RESTART_COMMAND_ENV_PREFIX_RE.match(stripped):
|
|
570
|
+
reasons.append("leading environment-variable assignment (FOO=bar ...)")
|
|
571
|
+
if reasons:
|
|
572
|
+
logger.warning(
|
|
573
|
+
"restart-command-shell-syntax",
|
|
574
|
+
extra={
|
|
575
|
+
"provider": self.name,
|
|
576
|
+
"restart_command": cmd,
|
|
577
|
+
"reasons": reasons,
|
|
578
|
+
"hint": (
|
|
579
|
+
"restart_command currently runs via "
|
|
580
|
+
"subprocess.run(..., shell=True); a planned "
|
|
581
|
+
"v2.12.0 change switches to shlex.split(...) + "
|
|
582
|
+
"shell=False, which does not support this "
|
|
583
|
+
"command's shell syntax. Rewrite it as plain "
|
|
584
|
+
"argv, or wrap it in a script and point "
|
|
585
|
+
"restart_command at that script."
|
|
586
|
+
),
|
|
587
|
+
},
|
|
588
|
+
)
|
|
589
|
+
return self
|
|
590
|
+
|
|
509
591
|
@model_validator(mode="after")
|
|
510
592
|
def _check_output_filters_known(self) -> ProviderConfig:
|
|
511
593
|
"""v1.0-A: fail at config-load on a typo'd filter name.
|