coderouter-cli 2.11.2__tar.gz → 2.13.0__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.2 → coderouter_cli-2.13.0}/CHANGELOG.md +295 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/PKG-INFO +1 -1
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/cli.py +20 -21
- coderouter_cli-2.13.0/coderouter/config/loader.py +264 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/config/schemas.py +143 -47
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/guards/context_budget.py +165 -20
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/guards/self_healing.py +58 -3
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/ingress/app.py +72 -29
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/ingress/launcher_routes.py +248 -62
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/launcher_devices.py +11 -4
- coderouter_cli-2.13.0/coderouter/token_estimation.py +441 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/concepts/context-budget.en.md +39 -2
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/concepts/context-budget.md +39 -2
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/subagent-routing.en.md +2 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/subagent-routing.md +2 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/examples/providers-multiagent.yaml +9 -1
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/examples/providers.raspberrypi.yaml +26 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/examples/providers.yaml +11 -2
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/pyproject.toml +37 -2
- coderouter_cli-2.13.0/tests/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/conftest.py +24 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_auto_router.py +75 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_cli.py +68 -0
- coderouter_cli-2.13.0/tests/test_config_cwd_warning.py +477 -0
- coderouter_cli-2.13.0/tests/test_context_budget.py +877 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fix_h8_launcher_auth.py +5 -4
- coderouter_cli-2.13.0/tests/test_fix_v213_launcher_hardening.py +604 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_ingress_count_tokens.py +135 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_backend_variants.py +5 -1
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_devices.py +14 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_gui_devices.py +5 -4
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_gui_readiness.py +4 -3
- coderouter_cli-2.13.0/tests/test_launcher_gui_sweep_close.py +444 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_variant_gui.py +7 -4
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_variant_sweep.py +8 -2
- coderouter_cli-2.13.0/tests/test_packaging.py +245 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_self_healing.py +157 -2
- coderouter_cli-2.13.0/tests/test_token_estimation.py +456 -0
- coderouter_cli-2.11.2/coderouter/config/loader.py +0 -165
- coderouter_cli-2.11.2/coderouter/token_estimation.py +0 -208
- coderouter_cli-2.11.2/tests/test_config_cwd_warning.py +0 -268
- coderouter_cli-2.11.2/tests/test_context_budget.py +0 -404
- coderouter_cli-2.11.2/tests/test_token_estimation.py +0 -135
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/.gitignore +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/LICENSE +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/README.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/README.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/adapters/anthropic_native.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/adapters/base.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/adapters/openai_compat.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/cost.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/errors.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/ingress/anthropic_routes.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/ingress/openai_routes.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/launcher_speculative.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/launcher_swap.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/logging.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/plugins/registry.py +0 -0
- /coderouter_cli-2.11.2/tests/__init__.py → /coderouter_cli-2.13.0/coderouter/py.typed +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/routing/capability.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/routing/fallback.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/state/audit_log.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/state/request_log.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/translation/convert.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/coderouter/vscode_init.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/README.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/README.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/external-agents.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/external-agents.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/gguf_dl.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/hf-ollama-models.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/launcher-quickstart.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/launcher.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/launcher.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/llamacpp-direct.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/llamacpp-direct.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/concepts/architecture.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/concepts/continuous-probing.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/concepts/drift-detection.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/concepts/partial-stitch.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/designs/agent-cli-plugin-extraction.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/designs/external-agents-adapter.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/designs/launcher-model-swap.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/designs/launcher-multi-build.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/designs/orchestration-companion.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/remote-access.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/remote-access.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/security.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/guides/vscode.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/low-memory-integration.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/examples/.env.example +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/examples/README.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/examples/providers-agent-cli.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/examples/providers.llamacpp-vllm.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/examples/providers.swap.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_agent_cli.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_budget.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_capability.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_capability_tool_choice.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_cli_bind_warning.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_config.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_config_shim_actions.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_errors.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_examples_yaml_roundtrip.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fallback_cache_strip.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fallback_empty_response.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fallback_tool_choice_emulate.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fix_h1_prometheus_drift.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fix_h2_h6_translation.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fix_h3_client_reuse.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fix_h4_h5_guards.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fix_h7_probe_restore.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fix_mA_routing.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fix_mB_adapters.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fix_mC_convert.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fix_mD_logging_metrics.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fix_mE_config_validation.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_fix_mF_ingress.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_import_hygiene.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_config_bench.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_devices_detect.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_devices_routes.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_gui_events.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_mtp.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_mtp_fallback.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_provider_sync.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_readiness_restart.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_swap.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_swap_review.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_variant_config.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_launcher_variant_routes.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_models_passthrough.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_plugin_adapter.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_stop_reason_forward_compat.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_tool_repair_differential.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_tool_repair_lenient.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_tool_repair_r4.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_tool_repair_scanners.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_toolrepair_bench.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_translation_reverse.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.13.0}/tests/test_vscode_init.py +0 -0
|
@@ -9,6 +9,301 @@ are kept verbatim where the Japanese text itself is the subject).
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## [v2.13.0] — 2026-08-07 (remaining-high security hardening)
|
|
13
|
+
|
|
14
|
+
### Changed (BREAKING)
|
|
15
|
+
|
|
16
|
+
- **Implicit `./providers.yaml` discovery is now opt-in, gated behind
|
|
17
|
+
`CODEROUTER_ALLOW_CWD_CONFIG`.** Previously, starting CodeRouter (or the
|
|
18
|
+
GUI launcher) from a directory containing a `providers.yaml` loaded that
|
|
19
|
+
file automatically. That was a code-execution vector: a hostile config
|
|
20
|
+
dropped into a repo could steer `restart_command`,
|
|
21
|
+
`launcher.backends[*].binary`, and `launcher.bench.command_template` —
|
|
22
|
+
all of which name executables — simply because of the working directory.
|
|
23
|
+
The current-working-directory step now runs only when
|
|
24
|
+
`CODEROUTER_ALLOW_CWD_CONFIG` is truthy (`1`/`true`/`yes`/`on`). When the
|
|
25
|
+
opt-in is off but a `./providers.yaml` exists and was not explicitly
|
|
26
|
+
named, a one-time `cwd-config-skipped` warning explains why it was
|
|
27
|
+
ignored and how to enable it (`--config`, `CODEROUTER_CONFIG`, or the
|
|
28
|
+
opt-in). To keep the old behaviour, set `CODEROUTER_ALLOW_CWD_CONFIG=1`
|
|
29
|
+
in directories you trust. (The v2.12.0 heads-up mis-stated the landing
|
|
30
|
+
release as "v2.12.0"; it actually lands here in v2.13.0.)
|
|
31
|
+
- **`restart_command` is now run as `subprocess.run(shlex.split(command),
|
|
32
|
+
shell=False)` — argv dispatch, no shell.** Self-healing previously ran
|
|
33
|
+
it through `shell=True`, so a `providers.yaml` from an untrusted source
|
|
34
|
+
meant arbitrary shell execution (directly, and unconditionally at
|
|
35
|
+
startup when excluded providers were restored from `state_dir`).
|
|
36
|
+
Pipelines, redirects, `~` expansion, and `FOO=bar cmd` env prefixes are
|
|
37
|
+
no longer supported. A value containing shell metacharacters (`&&`,
|
|
38
|
+
`||`, `|`, `;`, `>`, `<`, `$(...)`, `` ` ``) is now **refused (not run)**
|
|
39
|
+
rather than half-executed — under `shell=False`, `touch a; b` would
|
|
40
|
+
otherwise create a file literally named `a;` and report a "successful"
|
|
41
|
+
restart. Wrap shell-dependent commands in a script, or write them as
|
|
42
|
+
`/bin/sh -c '...'` explicitly.
|
|
43
|
+
- **`doctor --apply` now writes back to exactly the file the loader read.**
|
|
44
|
+
The apply path's config resolution was a hand-maintained copy of the
|
|
45
|
+
loader's search order; it now delegates to the loader's single
|
|
46
|
+
`resolve_config_path`, so the v2.13.0 CWD change can never make `--apply`
|
|
47
|
+
rewrite a file that `load_config` did not actually load.
|
|
48
|
+
- **The GUI launcher (`launcher_gui.py`) applies the same
|
|
49
|
+
`CODEROUTER_ALLOW_CWD_CONFIG` gate** to its own `providers.yaml`
|
|
50
|
+
discovery.
|
|
51
|
+
- **The launcher sweep API no longer accepts a `bench_command` (or
|
|
52
|
+
`results_dir`) from the request body.** `POST /api/launcher/sweep/start`
|
|
53
|
+
previously took an arbitrary command string, `shlex.split` it, and ran
|
|
54
|
+
it — so anyone who could reach the (loopback, token-optional) launcher
|
|
55
|
+
port could run any program under the CodeRouter process. The request now
|
|
56
|
+
carries only a `bench_preset` key that must name an entry under
|
|
57
|
+
`launcher.bench.presets` in `providers.yaml`; the executable template
|
|
58
|
+
lives solely in the operator-owned config, matching the trust boundary
|
|
59
|
+
already used for `launcher.backends[*].binary`. `bench_command` /
|
|
60
|
+
`results_dir` in a request are rejected with `400`. To keep a custom
|
|
61
|
+
sweep command, declare it under `launcher.bench.presets`; a single
|
|
62
|
+
`launcher.bench.command_template` still works unchanged as the implicit
|
|
63
|
+
`default` preset. Placeholder substitution in bench templates now splits
|
|
64
|
+
the template into argv *before* substituting, so a config label can no
|
|
65
|
+
longer inject extra arguments.
|
|
66
|
+
|
|
67
|
+
### Fixed (security)
|
|
68
|
+
|
|
69
|
+
- **The launcher HTML no longer embeds `CODEROUTER_LAUNCHER_TOKEN`.**
|
|
70
|
+
`GET /launcher` used to substitute the shared secret straight into the
|
|
71
|
+
page, so `curl /launcher | grep` recovered it and defeated
|
|
72
|
+
`_require_launcher_token`. The page now receives only a boolean telling
|
|
73
|
+
the UI whether auth is enabled; the operator enters the token once and it
|
|
74
|
+
is held in `sessionStorage` (per-tab). Token-free deployments are
|
|
75
|
+
unaffected.
|
|
76
|
+
- **`POST /api/launcher/start` and `/sweep/start` now validate
|
|
77
|
+
`model_path` / `draft_model_path` against `launcher.model_dirs`** (the
|
|
78
|
+
same check previously applied only to `/suggest`). When `model_dirs` is
|
|
79
|
+
unset the endpoints stay open as before, with a one-time warning;
|
|
80
|
+
configure `model_dirs` when the launcher is reachable beyond loopback.
|
|
81
|
+
- **Stored XSS in the launcher process table is fixed.** Process-row
|
|
82
|
+
buttons no longer build `onclick` attributes from the process name
|
|
83
|
+
(which an HTML parser decoded back into executable JS); they use
|
|
84
|
+
`data-*` attributes with a single delegated listener, so a name like
|
|
85
|
+
`x');alert(1);//` renders as inert text.
|
|
86
|
+
- **The request body-size limit is now enforced for
|
|
87
|
+
`Transfer-Encoding: chunked` requests.** The middleware previously only
|
|
88
|
+
read `Content-Length`, so a chunked body bypassed the cap entirely (and
|
|
89
|
+
a malformed length header fell through as unlimited). It now also counts
|
|
90
|
+
bytes as they are received and aborts with `413` the moment the running
|
|
91
|
+
total exceeds the cap, without buffering the body.
|
|
92
|
+
- **The GUI bench-sweep window no longer orphans `llama-server`
|
|
93
|
+
processes.** Closing the sweep window (the `×` button) did not signal the
|
|
94
|
+
worker, and sweep child processes were never registered for the app's
|
|
95
|
+
bulk shutdown, so backends kept running and leaked VRAM and ports. The
|
|
96
|
+
window now stops the worker and its children on close (non-blocking, via
|
|
97
|
+
an `after()` poll with a grace period then `SIGKILL`), sweep children are
|
|
98
|
+
drained when the app closes, and the app confirms before quitting while a
|
|
99
|
+
sweep is running.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## [v2.12.0] — 2026-08-04 (the context-budget guard actually fires now)
|
|
104
|
+
|
|
105
|
+
**This release changes behaviour you can observe.** The token estimator
|
|
106
|
+
counted `tool_result`, `tool_use` and `thinking` blocks as zero
|
|
107
|
+
characters, which meant the L1 context-budget guard stayed silent on
|
|
108
|
+
exactly the sessions it exists to protect — agent conversations, where
|
|
109
|
+
tool output is most of the context. Fixing the estimator makes the guard
|
|
110
|
+
fire, the auto-router's `content_token_count_min` rules match earlier,
|
|
111
|
+
and `POST /v1/messages/count_tokens` return larger numbers.
|
|
112
|
+
|
|
113
|
+
Nothing was removed and no field changed meaning, so configs load
|
|
114
|
+
unchanged. But if you set `context_budget_action: trim`, or wrote a
|
|
115
|
+
`content_token_count_min` threshold, **read the migration note below
|
|
116
|
+
before upgrading.** `token_estimation_include_tool_content: false`
|
|
117
|
+
restores the v2.11.x estimate exactly.
|
|
118
|
+
|
|
119
|
+
### Fixed
|
|
120
|
+
|
|
121
|
+
- **The token estimator ignored tool blocks, so the context-budget guard
|
|
122
|
+
never fired for agent sessions.** `_extract_text_from_content` only
|
|
123
|
+
summed `type: "text"` blocks. A `tool_result` carrying 5,000 characters
|
|
124
|
+
of file content counted as 0, and so did a `tool_use` input. In a
|
|
125
|
+
Claude Code shaped session — short assistant text, large tool results —
|
|
126
|
+
that under-counted by 8x at 20 turns, 14x at 50 and **24x at 200**, so
|
|
127
|
+
a conversation genuinely at 96,311 tokens was reported as 4,011 and the
|
|
128
|
+
0.80 / 0.90 thresholds were never approached. `context_budget_action:
|
|
129
|
+
trim` was, in practice, inert for the workload it was written for. The
|
|
130
|
+
estimator now walks blocks by type: `text`, `tool_result` (recursing
|
|
131
|
+
into a list-shaped `content`), `tool_use` (name plus its serialised
|
|
132
|
+
input) and `thinking` are counted. **`image` blocks and
|
|
133
|
+
`redacted_thinking.data` stay at zero** and this is load-bearing — a
|
|
134
|
+
naive `json.dumps` over the whole block would have inflated a session
|
|
135
|
+
holding one 400 KB base64 PNG by 7.1x. Unknown block types are skipped
|
|
136
|
+
rather than raising, and a self-referential structure terminates
|
|
137
|
+
(`token_estimation.py`).
|
|
138
|
+
- **Trimming could empty the conversation entirely.** `_normalize_head`
|
|
139
|
+
drops leading `assistant` messages and `user` messages that carry a
|
|
140
|
+
`tool_result`, because neither is a valid opening turn. When the
|
|
141
|
+
preserved tail was `[assistant(tool_use), user(tool_result), …]` — the
|
|
142
|
+
normal shape of an agent conversation — every message matched and the
|
|
143
|
+
trimmer returned an empty list. `AnthropicRequest.messages` has no
|
|
144
|
+
`min_length`, so pydantic accepted it and the upstream API answered
|
|
145
|
+
400. Measured on the shipped code: 81 messages in, 0 out. This was
|
|
146
|
+
reachable before this release too, but only for text-heavy sessions,
|
|
147
|
+
because tool blocks estimated as zero meant trimming rarely engaged.
|
|
148
|
+
Making the estimator honest would have made it routine. There is now
|
|
149
|
+
no path through the trimmer that returns an empty list for a non-empty
|
|
150
|
+
input (`guards/context_budget.py`).
|
|
151
|
+
- **Restoring a head message could undo the entire trim.** When
|
|
152
|
+
normalisation left no valid opening turn, the trimmer restored the most
|
|
153
|
+
recent clean `user` message it had just dropped. If that message was
|
|
154
|
+
the reason the request was over budget — a pasted file, a log dump —
|
|
155
|
+
restoring it put the request straight back over the window while
|
|
156
|
+
reporting `status: "trimmed"`. On a 200 KB paste followed by 25 tool
|
|
157
|
+
turns: 67,066 tokens in, 67,060 out, against a 32,768 window. Across a
|
|
158
|
+
300-session sweep, **89% of the results that still exceeded the window
|
|
159
|
+
were caused by this restore**, none by the preserve floor. Restoration
|
|
160
|
+
now checks the budget: the most recent *affordable* dropped clean user
|
|
161
|
+
wins, and if none fits, a synthetic
|
|
162
|
+
`[earlier conversation trimmed to fit the context budget]` head is
|
|
163
|
+
used instead. Same fixture: 67,066 → 17,062, inside both the window and
|
|
164
|
+
the trim target. The sweep goes to 0% (`guards/context_budget.py`).
|
|
165
|
+
- **The trim loop was quadratic.** It rebuilt the surviving message list
|
|
166
|
+
and re-scanned every character for each unit it considered dropping.
|
|
167
|
+
With the corrected estimator that is 217 ms of blocked event loop for
|
|
168
|
+
an 801-message conversation, and it is called synchronously from the
|
|
169
|
+
request path. Per-message character counts are now computed once and
|
|
170
|
+
decremented as units are dropped: 217 ms → 4 ms. Units that would free
|
|
171
|
+
nothing are skipped rather than dropped, so the trimmer no longer
|
|
172
|
+
destroys history without reducing the estimate. The set of dropped
|
|
173
|
+
messages is unchanged — verified against the old algorithm across 200
|
|
174
|
+
random conversations (`guards/context_budget.py`).
|
|
175
|
+
- **The readiness probe now targets the `127.0.0.1` literal rather than
|
|
176
|
+
`localhost`.** Both copies of `_backend_ready` fetched
|
|
177
|
+
`http://localhost:<port>/health` while the backends they probe listen
|
|
178
|
+
on IPv4 only (`llama-server --host 127.0.0.1` by default), and the bare
|
|
179
|
+
TCP fallback in the same function already used the literal — so the
|
|
180
|
+
HTTP branch was the odd one out. On a host with no IPv6 stack at all,
|
|
181
|
+
where `localhost` resolves to `::1` and nothing else, this fails every
|
|
182
|
+
attempt with `Address family not supported` and a backend that is up
|
|
183
|
+
and serving never registers. Hardening rather than a reported bug: on a
|
|
184
|
+
host that has `::1`, the connect is refused immediately and the
|
|
185
|
+
resolver falls through to IPv4, so the old form worked. Applied to both
|
|
186
|
+
`ingress/launcher_routes.py` and `launcher_gui.py`, which carry
|
|
187
|
+
independent copies of this function.
|
|
188
|
+
|
|
189
|
+
### Added
|
|
190
|
+
|
|
191
|
+
- **`token_estimation_include_tool_content`** (top level, default
|
|
192
|
+
`true`). Set it to `false` to get the v2.11.x estimate back, byte for
|
|
193
|
+
byte — verified identical across 300 fuzzed request bodies on all three
|
|
194
|
+
estimator entry points. It is an escape hatch for operators who tuned
|
|
195
|
+
thresholds against the old numbers and need time to retune; expect it
|
|
196
|
+
to be removed in a future major. It is deliberately not per-profile:
|
|
197
|
+
the auto-router runs *before* a profile is chosen, and `count_tokens`
|
|
198
|
+
answers a client question that must not depend on which chain would
|
|
199
|
+
have served the request (`config/schemas.py`).
|
|
200
|
+
- **`coderouter/py.typed`.** The package has advertised
|
|
201
|
+
`Typing :: Typed` in its classifiers without shipping a PEP 561
|
|
202
|
+
marker, so type checkers treated it as untyped. Verified present in the
|
|
203
|
+
built wheel (`pyproject.toml`).
|
|
204
|
+
- **Release-time version and changelog checks.** `release.yml` now fails
|
|
205
|
+
the build if the pushed tag does not match `pyproject.toml`'s version,
|
|
206
|
+
or if `CHANGELOG.md` has no section for it — the awk that extracts
|
|
207
|
+
release notes silently falls back to a placeholder otherwise. Skipped
|
|
208
|
+
on `workflow_dispatch`, where `github.ref_name` is a branch.
|
|
209
|
+
|
|
210
|
+
### Changed
|
|
211
|
+
|
|
212
|
+
- **`examples/providers.yaml` and `examples/providers-multiagent.yaml`
|
|
213
|
+
now ship `context_budget_action: warn` instead of `trim`.** README
|
|
214
|
+
tells new users to `curl` the first of those, so shipping `trim`
|
|
215
|
+
together with a newly-working guard would have started deleting
|
|
216
|
+
history in conversations for people who never opted into it. Warn
|
|
217
|
+
first, read the logs, then decide. `docs/concepts/context-budget.md`
|
|
218
|
+
was updated to match.
|
|
219
|
+
- **`thinking` blocks are now counted.** Extended-thinking content is
|
|
220
|
+
replayed to the model on the following turn — it has to be, or tool-use
|
|
221
|
+
signatures break — so it occupies the window exactly like text.
|
|
222
|
+
`redacted_thinking.data` is still not counted: it is opaque ciphertext,
|
|
223
|
+
the same category as base64 image data.
|
|
224
|
+
- **Third-party GitHub Actions are pinned to commit SHAs**, with the
|
|
225
|
+
version in a trailing comment so dependabot still tracks them. The
|
|
226
|
+
publish job holds `id-token: write` for Trusted Publishing and the
|
|
227
|
+
release job holds `contents: write`; a moved tag on any action they
|
|
228
|
+
use is a direct path to the PyPI project.
|
|
229
|
+
- **A macOS CI runner was added and then withdrawn again before this
|
|
230
|
+
release shipped.** It failed six launcher tests immediately, and the
|
|
231
|
+
failures turned out to belong to the test harness rather than the
|
|
232
|
+
product: every test that spawns a real stub process and requires its
|
|
233
|
+
`/health` to answer 200 within the harness's 5.0s
|
|
234
|
+
`readiness_timeout_s` failed, and no others. Spawning, argv
|
|
235
|
+
construction, crash detection, SIGTERM handling, TTL and registry
|
|
236
|
+
behaviour all passed on macOS. That 5.0s budget is 1/60th of the
|
|
237
|
+
shipped default of 300.0s, so it does not represent any real
|
|
238
|
+
deployment. Rather than ship six `xfail`s or a red CI, the runner is
|
|
239
|
+
deferred to a follow-up that raises the harness budget first. The
|
|
240
|
+
reasoning and the re-enable steps are recorded in `ci.yml` next to the
|
|
241
|
+
matrix.
|
|
242
|
+
- **Corrected the note about tkinter on CI**, which v2.11.2 got wrong in
|
|
243
|
+
both directions. The GUI test modules originally said the uv-managed
|
|
244
|
+
Python on CI has no tkinter; v2.11.2 "fixed" that to say it does,
|
|
245
|
+
based on measuring `uv python install 3.12.11` in a Linux container.
|
|
246
|
+
The actual CI runs settle it: the ubuntu runner skips those four files
|
|
247
|
+
and the macOS runner ran them. Tk availability is a property of the
|
|
248
|
+
runner image, so the docstrings no longer assert it in either
|
|
249
|
+
direction — they just say to keep the `importorskip`.
|
|
250
|
+
- **The CVE audit covers every extra.** `uv export` passed only
|
|
251
|
+
`--extra dev`, leaving `accuracy` (tokenizers) and `repair`
|
|
252
|
+
(json-repair) — both installable by users — outside pip-audit's view.
|
|
253
|
+
Now `--all-extras`.
|
|
254
|
+
- **The sdist no longer allow-lists `docs` wholesale.** `only-include`
|
|
255
|
+
named the whole directory, so a local `uv build` swept in
|
|
256
|
+
`docs/inside/` and `docs/articles/` — private notes and article drafts
|
|
257
|
+
that `.gitignore` exists to keep out. A CI build from a clean checkout
|
|
258
|
+
was never affected, since those files are untracked, but a maintainer
|
|
259
|
+
building from a working tree would have shipped them. The list now
|
|
260
|
+
names the public subdirectories, and `release.yml` fails the build if
|
|
261
|
+
either directory reappears in the tarball. Every entry has to be
|
|
262
|
+
git-tracked: `docs/evidence` exists only in a maintainer's working tree
|
|
263
|
+
and made the allowlist claim to ship something a fresh checkout does
|
|
264
|
+
not have. `tests/test_packaging.py` now asserts each listed path
|
|
265
|
+
exists, which is how that was caught.
|
|
266
|
+
|
|
267
|
+
### Migration
|
|
268
|
+
|
|
269
|
+
Only two settings change meaning. If you set neither, upgrading is a
|
|
270
|
+
no-op beyond larger `count_tokens` responses.
|
|
271
|
+
|
|
272
|
+
**`context_budget_action: trim` or `warn`** — the guard was effectively
|
|
273
|
+
inert for tool-heavy sessions and now works. Expect
|
|
274
|
+
`context-budget-warning` in the logs, the
|
|
275
|
+
`X-CodeRouter-Context-Budget` response header, and — under `trim` —
|
|
276
|
+
conversation history actually being dropped, with a
|
|
277
|
+
`[earlier conversation trimmed to fit the context budget]` message
|
|
278
|
+
appearing at the head when nothing else fits. Measured against a 32 K
|
|
279
|
+
window, a tool-driven session now warns around turn 32 and trims around
|
|
280
|
+
turn 36; before, it never did either. Start with `warn` and read the
|
|
281
|
+
logs before enabling `trim`.
|
|
282
|
+
|
|
283
|
+
**`content_token_count_min`** — auto-router rules using it will match far
|
|
284
|
+
earlier. A tool-driven session that reported 1,083 tokens after 200 turns
|
|
285
|
+
under v2.11.x reports 93,183 now, so a `32000` threshold moves from never
|
|
286
|
+
firing to firing around turn 69. Check where such a rule routes:
|
|
287
|
+
`examples/providers.raspberrypi.yaml` sends matching traffic to a cloud
|
|
288
|
+
free tier rather than the local model. Retune the threshold, or set
|
|
289
|
+
`token_estimation_include_tool_content: false` while you do.
|
|
290
|
+
|
|
291
|
+
### Tests
|
|
292
|
+
|
|
293
|
+
- 3 new files, 48 new cases. Suite: 2393 passed, 1 skipped, ruff clean.
|
|
294
|
+
- Each fix has a regression test confirmed to fail against the pre-fix
|
|
295
|
+
tree, including `test_trim_never_returns_empty_messages` (81 in, 0 out
|
|
296
|
+
on the old code) and `test_trim_lands_inside_the_window_across_budgets`,
|
|
297
|
+
which asserts that if a trimmed request is still over the window, the
|
|
298
|
+
preserve floor alone must account for it — structurally forbidding the
|
|
299
|
+
restore branch from being the cause.
|
|
300
|
+
- The incremental trim loop is pinned against the old full-recompute
|
|
301
|
+
implementation across 200 random conversations (identical dropped
|
|
302
|
+
sets), and the opt-out is pinned against v2.11.2 across 300 fuzzed
|
|
303
|
+
bodies.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
12
307
|
## [v2.11.2] — 2026-08-04 (file-write safety and launcher event hygiene)
|
|
13
308
|
|
|
14
309
|
Two fixes from the same review round as v2.11.1, split out because they
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.13.0
|
|
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
|
|
@@ -600,28 +600,27 @@ def _run_check_model(args: argparse.Namespace) -> int:
|
|
|
600
600
|
|
|
601
601
|
|
|
602
602
|
def _resolve_config_path(explicit: str | None) -> Path:
|
|
603
|
-
"""
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
603
|
+
"""Return the file the loader actually read, for ``--apply`` write-back.
|
|
604
|
+
|
|
605
|
+
Delegates the search order to
|
|
606
|
+
:func:`coderouter.config.loader.resolve_config_path` rather than
|
|
607
|
+
re-implementing it. Re-implementing it here is exactly how the
|
|
608
|
+
v2.13.0 CWD opt-in change would introduce a silent bug: if the loader
|
|
609
|
+
gates ``./providers.yaml`` behind ``CODEROUTER_ALLOW_CWD_CONFIG`` but
|
|
610
|
+
this copy still searched CWD unconditionally, ``doctor --apply`` would
|
|
611
|
+
write patches back into a file that ``load_config`` never read. Keeping
|
|
612
|
+
one source of truth makes that impossible.
|
|
613
|
+
|
|
614
|
+
When nothing exists, falls back to ``~/.coderouter/providers.yaml``
|
|
615
|
+
(the loader's last candidate) — the apply step surfaces a clearer
|
|
616
|
+
error against that path than this resolver would.
|
|
609
617
|
"""
|
|
610
|
-
import
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
candidates.append(Path(env_path))
|
|
617
|
-
candidates.append(Path.cwd() / "providers.yaml")
|
|
618
|
-
candidates.append(Path.home() / ".coderouter" / "providers.yaml")
|
|
619
|
-
for p in candidates:
|
|
620
|
-
if p.is_file():
|
|
621
|
-
return p
|
|
622
|
-
# Fall back to the last candidate even if absent — the apply step
|
|
623
|
-
# will surface a clearer error than this resolver would.
|
|
624
|
-
return candidates[-1]
|
|
618
|
+
from coderouter.config.loader import resolve_config_path
|
|
619
|
+
|
|
620
|
+
resolved = resolve_config_path(explicit)
|
|
621
|
+
if resolved is not None:
|
|
622
|
+
return resolved
|
|
623
|
+
return Path.home() / ".coderouter" / "providers.yaml"
|
|
625
624
|
|
|
626
625
|
|
|
627
626
|
def _write_verdict_line(*, write: bool, files_written: int) -> None:
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Load and validate CodeRouter configuration.
|
|
2
|
+
|
|
3
|
+
Search order (first hit wins):
|
|
4
|
+
1. Path passed explicitly (CLI --config flag)
|
|
5
|
+
2. $CODEROUTER_CONFIG env var
|
|
6
|
+
3. ./providers.yaml (current working dir) — opt-in since v2.13.0,
|
|
7
|
+
gated behind ``CODEROUTER_ALLOW_CWD_CONFIG`` (truthy: 1/true/yes/on)
|
|
8
|
+
4. ~/.coderouter/providers.yaml
|
|
9
|
+
|
|
10
|
+
Secrets are resolved by reading the env var named by `api_key_env`.
|
|
11
|
+
|
|
12
|
+
.. note::
|
|
13
|
+
**v2.13.0 (security):** step 3 above (implicit CWD discovery) is now
|
|
14
|
+
opt-in, gated behind ``CODEROUTER_ALLOW_CWD_CONFIG``. It was a code
|
|
15
|
+
execution vector: a hostile ``providers.yaml`` dropped into a repo
|
|
16
|
+
could steer ``restart_command`` / ``launcher.backends[*].binary`` /
|
|
17
|
+
``launcher.bench.command_template`` — all of which name executables —
|
|
18
|
+
simply because CodeRouter happened to be started from that directory.
|
|
19
|
+
When the opt-in is enabled and step 3 resolves the config, a one-time
|
|
20
|
+
``cwd-config-loaded`` warning fires (never when an explicit
|
|
21
|
+
``--config`` / ``CODEROUTER_CONFIG`` path was used, even if it happens
|
|
22
|
+
to point at the same file). When the opt-in is NOT set but a
|
|
23
|
+
``./providers.yaml`` exists and was not explicitly named, a one-time
|
|
24
|
+
``cwd-config-skipped`` warning fires so the operator understands why
|
|
25
|
+
that file was ignored.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import os
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
import yaml
|
|
34
|
+
|
|
35
|
+
from coderouter.config.schemas import CodeRouterConfig
|
|
36
|
+
from coderouter.logging import get_logger
|
|
37
|
+
|
|
38
|
+
logger = get_logger(__name__)
|
|
39
|
+
|
|
40
|
+
# Name of the env var that opts CWD ``providers.yaml`` discovery back in
|
|
41
|
+
# (see module docstring). v2.13.0 gates step 3 of the search behind it.
|
|
42
|
+
CWD_CONFIG_ENV = "CODEROUTER_ALLOW_CWD_CONFIG"
|
|
43
|
+
# Truthy vocabulary shared by every CWD-opt-in / ALLOW_PAID style toggle.
|
|
44
|
+
_TRUTHY = {"1", "true", "yes", "on"}
|
|
45
|
+
|
|
46
|
+
# Guards the one-time ``cwd-config-loaded`` warning (opt-in enabled and the
|
|
47
|
+
# CWD step actually resolved) so a process that calls load_config()
|
|
48
|
+
# repeatedly (tests, hot-reload, multiple create_app() calls) only logs it
|
|
49
|
+
# once.
|
|
50
|
+
_cwd_config_warning_emitted = False
|
|
51
|
+
# Guards the one-time ``cwd-config-skipped`` warning (opt-in NOT set but a
|
|
52
|
+
# ./providers.yaml exists and was ignored) — same once-per-process rationale.
|
|
53
|
+
_cwd_config_skip_warning_emitted = False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cwd_config_allowed() -> bool:
|
|
57
|
+
"""True iff the CWD ``providers.yaml`` opt-in is enabled (v2.13.0)."""
|
|
58
|
+
return os.environ.get(CWD_CONFIG_ENV, "").strip().lower() in _TRUTHY
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _candidate_paths(explicit: str | os.PathLike[str] | None) -> list[Path]:
|
|
62
|
+
paths: list[Path] = []
|
|
63
|
+
if explicit:
|
|
64
|
+
paths.append(Path(explicit))
|
|
65
|
+
if env_path := os.environ.get("CODEROUTER_CONFIG"):
|
|
66
|
+
paths.append(Path(env_path))
|
|
67
|
+
# v2.13.0: implicit CWD discovery is opt-in — a hostile providers.yaml
|
|
68
|
+
# dropped into a repo could otherwise steer restart_command / launcher
|
|
69
|
+
# binaries simply because CodeRouter was started from that directory.
|
|
70
|
+
if cwd_config_allowed():
|
|
71
|
+
paths.append(Path.cwd() / "providers.yaml")
|
|
72
|
+
paths.append(Path.home() / ".coderouter" / "providers.yaml")
|
|
73
|
+
return paths
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_config_path(
|
|
77
|
+
explicit: str | os.PathLike[str] | None = None,
|
|
78
|
+
) -> Path | None:
|
|
79
|
+
"""Return the config file the loader would pick, or None if none exists.
|
|
80
|
+
|
|
81
|
+
Single source of truth for the search order so callers (e.g.
|
|
82
|
+
``coderouter.cli._resolve_config_path`` driving ``doctor --apply``)
|
|
83
|
+
never re-implement it. Re-implementing it risks writing back to a
|
|
84
|
+
different file than the one :func:`load_config` actually read — the
|
|
85
|
+
exact regression the v2.13.0 CWD opt-in would otherwise introduce.
|
|
86
|
+
"""
|
|
87
|
+
return next((p for p in _candidate_paths(explicit) if p.is_file()), None)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _explicitly_named(
|
|
91
|
+
path: Path, explicit: str | os.PathLike[str] | None
|
|
92
|
+
) -> bool:
|
|
93
|
+
"""True iff ``path`` matches the literal --config or CODEROUTER_CONFIG value.
|
|
94
|
+
|
|
95
|
+
Used to keep the CWD warnings quiet when ``./providers.yaml`` was
|
|
96
|
+
chosen on purpose (an explicit choice, however coincidental, is not
|
|
97
|
+
the implicit-discovery behaviour being gated).
|
|
98
|
+
"""
|
|
99
|
+
if explicit and Path(explicit) == path:
|
|
100
|
+
return True
|
|
101
|
+
env_path = os.environ.get("CODEROUTER_CONFIG")
|
|
102
|
+
return env_path is not None and Path(env_path) == path
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _warn_if_cwd_config(
|
|
106
|
+
chosen: Path, *, explicit: str | os.PathLike[str] | None
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Emit the one-time ``cwd-config-loaded`` warning (opt-in is enabled).
|
|
109
|
+
|
|
110
|
+
Only fires when ``chosen`` was resolved via the *implicit* CWD-search
|
|
111
|
+
step — which, since v2.13.0, only runs when
|
|
112
|
+
``CODEROUTER_ALLOW_CWD_CONFIG`` is set. Compares ``chosen`` against the
|
|
113
|
+
*literal* explicit / env candidate paths (not merely whether those
|
|
114
|
+
inputs were set) so that an explicit ``--config`` (or
|
|
115
|
+
``CODEROUTER_CONFIG``) path that happens to coincide with
|
|
116
|
+
``./providers.yaml`` stays quiet, while an explicit path/env var that
|
|
117
|
+
was set but did NOT resolve (file missing, search fell through to CWD)
|
|
118
|
+
still warns — the CWD step is what actually served the config there.
|
|
119
|
+
"""
|
|
120
|
+
global _cwd_config_warning_emitted
|
|
121
|
+
if _cwd_config_warning_emitted:
|
|
122
|
+
return
|
|
123
|
+
cwd_path = Path.cwd() / "providers.yaml"
|
|
124
|
+
if chosen != cwd_path:
|
|
125
|
+
return
|
|
126
|
+
if _explicitly_named(chosen, explicit):
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
_cwd_config_warning_emitted = True
|
|
130
|
+
logger.warning(
|
|
131
|
+
"cwd-config-loaded",
|
|
132
|
+
extra={
|
|
133
|
+
"path": str(chosen),
|
|
134
|
+
"hint": (
|
|
135
|
+
f"loaded providers.yaml from the current working "
|
|
136
|
+
f"directory ({chosen}) because CODEROUTER_ALLOW_CWD_CONFIG "
|
|
137
|
+
"opt-in is enabled. This file's restart_command / "
|
|
138
|
+
"launcher.backends[*].binary / "
|
|
139
|
+
"launcher.bench.command_template decide which executables "
|
|
140
|
+
"run — only enable the opt-in in directories you trust."
|
|
141
|
+
),
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _warn_if_cwd_config_skipped(
|
|
147
|
+
explicit: str | os.PathLike[str] | None,
|
|
148
|
+
) -> Path | None:
|
|
149
|
+
"""Warn once when a ./providers.yaml exists but was skipped (no opt-in).
|
|
150
|
+
|
|
151
|
+
Returns the skipped CWD path (so :func:`load_config` can name it in a
|
|
152
|
+
later ``FileNotFoundError``), or None when there is nothing to skip:
|
|
153
|
+
the opt-in is enabled, no ``./providers.yaml`` exists, or the file was
|
|
154
|
+
explicitly named (``--config`` / ``CODEROUTER_CONFIG``) — in which case
|
|
155
|
+
it is not being skipped at all.
|
|
156
|
+
"""
|
|
157
|
+
global _cwd_config_skip_warning_emitted
|
|
158
|
+
if cwd_config_allowed():
|
|
159
|
+
return None
|
|
160
|
+
cwd_path = Path.cwd() / "providers.yaml"
|
|
161
|
+
if not cwd_path.is_file():
|
|
162
|
+
return None
|
|
163
|
+
if _explicitly_named(cwd_path, explicit):
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
if not _cwd_config_skip_warning_emitted:
|
|
167
|
+
_cwd_config_skip_warning_emitted = True
|
|
168
|
+
logger.warning(
|
|
169
|
+
"cwd-config-skipped",
|
|
170
|
+
extra={
|
|
171
|
+
"path": str(cwd_path),
|
|
172
|
+
"hint": (
|
|
173
|
+
f"found providers.yaml in the current working "
|
|
174
|
+
f"directory ({cwd_path}) but did NOT load it: implicit "
|
|
175
|
+
"CWD discovery is opt-in since v2.13.0 (a hostile "
|
|
176
|
+
"providers.yaml can steer restart_command / launcher "
|
|
177
|
+
"binaries). To use it, set CODEROUTER_ALLOW_CWD_CONFIG=1 "
|
|
178
|
+
"(only in directories you trust) or pass --config "
|
|
179
|
+
f"{cwd_path} to name it explicitly."
|
|
180
|
+
),
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
return cwd_path
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def load_config(path: str | os.PathLike[str] | None = None) -> CodeRouterConfig:
|
|
187
|
+
"""Load providers.yaml + apply ALLOW_PAID env override."""
|
|
188
|
+
skipped_cwd = _warn_if_cwd_config_skipped(path)
|
|
189
|
+
candidates = _candidate_paths(path)
|
|
190
|
+
chosen: Path | None = next((p for p in candidates if p.is_file()), None)
|
|
191
|
+
if chosen is None:
|
|
192
|
+
searched = "\n ".join(str(p) for p in candidates)
|
|
193
|
+
message = (
|
|
194
|
+
f"providers.yaml not found. Searched:\n {searched}\n"
|
|
195
|
+
f"Hint: copy examples/providers.yaml to ~/.coderouter/providers.yaml"
|
|
196
|
+
)
|
|
197
|
+
if skipped_cwd is not None:
|
|
198
|
+
message += (
|
|
199
|
+
f"\nNote: {skipped_cwd} exists but was NOT read — implicit "
|
|
200
|
+
"CWD discovery is opt-in since v2.13.0. Three ways to use "
|
|
201
|
+
f"it: pass --config {skipped_cwd}, set "
|
|
202
|
+
f"CODEROUTER_CONFIG={skipped_cwd}, or set "
|
|
203
|
+
"CODEROUTER_ALLOW_CWD_CONFIG=1 (only in directories you trust)."
|
|
204
|
+
)
|
|
205
|
+
raise FileNotFoundError(message)
|
|
206
|
+
_warn_if_cwd_config(chosen, explicit=path)
|
|
207
|
+
|
|
208
|
+
with chosen.open("r", encoding="utf-8") as f:
|
|
209
|
+
raw = yaml.safe_load(f) or {}
|
|
210
|
+
|
|
211
|
+
# v0.6-A: CODEROUTER_MODE env overrides the YAML default_profile BEFORE
|
|
212
|
+
# initial validation, so that (a) a typo'd file default that would otherwise
|
|
213
|
+
# fail can be rescued by an explicit env-set mode, and (b) the model-
|
|
214
|
+
# validator's "default_profile must exist in profiles" check applies to the
|
|
215
|
+
# *effective* mode the engine will see, not the pre-override YAML value.
|
|
216
|
+
#
|
|
217
|
+
# v1.8.0+: also resolve env_mode through ``mode_aliases`` before assigning,
|
|
218
|
+
# so that startup-time ``--mode coding`` (env CODEROUTER_MODE=coding)
|
|
219
|
+
# behaves symmetrically with the runtime ``X-CodeRouter-Mode: coding``
|
|
220
|
+
# header — both should accept short intent names like ``coding`` /
|
|
221
|
+
# ``general`` / ``reasoning`` and resolve them to the underlying profile
|
|
222
|
+
# (e.g. ``claude-code-nim`` in providers.nvidia-nim.yaml). Without this,
|
|
223
|
+
# users on the NIM example yaml hit
|
|
224
|
+
# "default_profile 'coding' is not declared in profiles:
|
|
225
|
+
# known=['claude-code-nim', ...]"
|
|
226
|
+
# because mode_aliases only fired at request time, not at startup.
|
|
227
|
+
env_mode = os.environ.get("CODEROUTER_MODE", "").strip()
|
|
228
|
+
if env_mode:
|
|
229
|
+
# Pre-validation alias resolution: if env_mode isn't directly a
|
|
230
|
+
# profile name but matches an entry in raw["mode_aliases"], swap it
|
|
231
|
+
# for the underlying profile name. This avoids forcing every example
|
|
232
|
+
# yaml to mirror the v1.8.0 four-profile names (multi/coding/general
|
|
233
|
+
# /reasoning) just to accept the canonical short --mode flags.
|
|
234
|
+
raw_profiles = raw.get("profiles", []) or []
|
|
235
|
+
profile_names = {
|
|
236
|
+
p.get("name") for p in raw_profiles if isinstance(p, dict)
|
|
237
|
+
}
|
|
238
|
+
raw_aliases = raw.get("mode_aliases", {}) or {}
|
|
239
|
+
if (
|
|
240
|
+
env_mode not in profile_names
|
|
241
|
+
and isinstance(raw_aliases, dict)
|
|
242
|
+
and env_mode in raw_aliases
|
|
243
|
+
):
|
|
244
|
+
env_mode = raw_aliases[env_mode]
|
|
245
|
+
raw["default_profile"] = env_mode
|
|
246
|
+
|
|
247
|
+
config = CodeRouterConfig.model_validate(raw)
|
|
248
|
+
|
|
249
|
+
# Env var ALLOW_PAID overrides file value (so users can flip it per-shell)
|
|
250
|
+
env_paid = os.environ.get("ALLOW_PAID", "").strip().lower()
|
|
251
|
+
if env_paid in {"1", "true", "yes", "on"}:
|
|
252
|
+
config.allow_paid = True
|
|
253
|
+
elif env_paid in {"0", "false", "no", "off"}:
|
|
254
|
+
config.allow_paid = False
|
|
255
|
+
|
|
256
|
+
return config
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def resolve_api_key(api_key_env: str | None) -> str | None:
|
|
260
|
+
"""Look up an API key from the named env var. Returns None if unset."""
|
|
261
|
+
if not api_key_env:
|
|
262
|
+
return None
|
|
263
|
+
value = os.environ.get(api_key_env, "").strip()
|
|
264
|
+
return value or None
|