coderouter-cli 2.13.0__tar.gz → 2.14.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.13.0 → coderouter_cli-2.14.0}/CHANGELOG.md +124 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/PKG-INFO +1 -1
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/adapters/anthropic_native.py +4 -2
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/adapters/openai_compat.py +5 -2
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/cli.py +140 -2
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/config/loader.py +24 -1
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/config/schemas.py +144 -0
- coderouter_cli-2.14.0/coderouter/credentials.py +332 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/doctor_apply.py +54 -9
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/ingress/dashboard_routes.py +24 -4
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/ingress/metrics_routes.py +13 -2
- coderouter_cli-2.14.0/coderouter/ingress/ui_auth.py +94 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/logging.py +7 -0
- coderouter_cli-2.14.0/coderouter/rollback.py +217 -0
- coderouter_cli-2.14.0/coderouter/secret_redaction.py +606 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/state/audit_log.py +6 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/state/request_log.py +6 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/vscode_init.py +156 -20
- coderouter_cli-2.14.0/examples/providers.cli-session.yaml +121 -0
- coderouter_cli-2.14.0/examples/providers.llamacpp-vllm.yaml +544 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/pyproject.toml +1 -1
- coderouter_cli-2.14.0/tests/test_credentials.py +348 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_doctor_apply.py +65 -0
- coderouter_cli-2.14.0/tests/test_rollback.py +195 -0
- coderouter_cli-2.14.0/tests/test_secret_redaction.py +340 -0
- coderouter_cli-2.14.0/tests/test_ui_auth.py +142 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_vscode_init.py +145 -3
- coderouter_cli-2.13.0/examples/providers.llamacpp-vllm.yaml +0 -332
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/.gitignore +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/LICENSE +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/README.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/README.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/adapters/base.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/cost.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/errors.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/guards/context_budget.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/guards/self_healing.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/ingress/anthropic_routes.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/ingress/launcher_routes.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/ingress/openai_routes.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/launcher_devices.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/launcher_speculative.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/launcher_swap.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/py.typed +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/routing/capability.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/routing/fallback.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/translation/convert.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/README.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/README.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/external-agents.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/external-agents.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/gguf_dl.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/hf-ollama-models.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/launcher-quickstart.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/launcher.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/launcher.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/llamacpp-direct.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/llamacpp-direct.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/concepts/architecture.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/concepts/context-budget.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/concepts/context-budget.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/concepts/continuous-probing.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/concepts/drift-detection.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/concepts/partial-stitch.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/designs/agent-cli-plugin-extraction.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/designs/external-agents-adapter.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/designs/launcher-model-swap.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/designs/launcher-multi-build.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/designs/orchestration-companion.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/remote-access.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/remote-access.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/security.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/subagent-routing.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/subagent-routing.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/guides/vscode.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/low-memory-integration.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/examples/.env.example +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/examples/README.md +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/examples/providers-agent-cli.yaml +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/examples/providers-multiagent.yaml +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/examples/providers.raspberrypi.yaml +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/examples/providers.swap.yaml +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/examples/providers.yaml +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/__init__.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/conftest.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_agent_cli.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_auto_router.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_budget.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_capability.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_capability_tool_choice.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_cli.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_cli_bind_warning.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_config.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_config_cwd_warning.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_config_shim_actions.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_context_budget.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_errors.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_examples_yaml_roundtrip.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fallback_cache_strip.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fallback_empty_response.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fallback_tool_choice_emulate.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fix_h1_prometheus_drift.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fix_h2_h6_translation.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fix_h3_client_reuse.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fix_h4_h5_guards.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fix_h7_probe_restore.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fix_h8_launcher_auth.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fix_mA_routing.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fix_mB_adapters.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fix_mC_convert.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fix_mD_logging_metrics.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fix_mE_config_validation.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fix_mF_ingress.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_fix_v213_launcher_hardening.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_import_hygiene.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_ingress_count_tokens.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_backend_variants.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_config_bench.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_devices.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_devices_detect.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_devices_routes.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_gui_devices.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_gui_events.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_gui_readiness.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_gui_sweep_close.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_mtp.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_mtp_fallback.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_provider_sync.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_readiness_restart.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_swap.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_swap_review.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_variant_config.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_variant_gui.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_variant_routes.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_launcher_variant_sweep.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_models_passthrough.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_packaging.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_plugin_adapter.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_stop_reason_forward_compat.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_tool_repair_differential.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_tool_repair_lenient.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_tool_repair_r4.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_tool_repair_scanners.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_toolrepair_bench.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.13.0 → coderouter_cli-2.14.0}/tests/test_translation_reverse.py +0 -0
|
@@ -9,6 +9,130 @@ are kept verbatim where the Japanese text itself is the subject).
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## [v2.14.0] — 2026-08-09 (credential hygiene + undo)
|
|
13
|
+
|
|
14
|
+
Five changes that came out of reading `duolahypercho/codex-router` and
|
|
15
|
+
comparing it with what CodeRouter actually does. Four of them close holes
|
|
16
|
+
this project had; one imports a capability it was missing. Everything is
|
|
17
|
+
opt-in except the `.envrc` behaviour change called out under **Changed
|
|
18
|
+
(BREAKING)**.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **`credential.source: cli_session` — borrow the token a vendor CLI
|
|
23
|
+
already wrote.** A subscription-authenticated provider (Kimi Code CLI,
|
|
24
|
+
Grok CLI, …) can now be an ordinary `openai_compat` / `anthropic`
|
|
25
|
+
entry: CodeRouter reads the OAuth token out of the JSON file the
|
|
26
|
+
vendor's CLI maintains and makes the HTTP call itself. Until now the
|
|
27
|
+
only answer to those was `kind: agent_cli`, which spawns the CLI once
|
|
28
|
+
per request and therefore has no streaming, no tool-call repair, no
|
|
29
|
+
context-budget guard, and does not sit in a fallback chain — the
|
|
30
|
+
subscription ended up an island. It is now a chain link:
|
|
31
|
+
`providers: [kimi-sub, free-cloud, local-llama]` just works.
|
|
32
|
+
Refresh delegates to the vendor's own CLI (`refresh.command`, an argv
|
|
33
|
+
list run with `shell=False`) rather than reimplementing OAuth, because
|
|
34
|
+
endpoints, client ids and rotation policies all change and a bespoke
|
|
35
|
+
implementation rots. Refresh is single-flighted in-process and
|
|
36
|
+
serialised across processes with an advisory `flock`. `credential` and
|
|
37
|
+
`api_key_env` are mutually exclusive at load time, `credential.path`
|
|
38
|
+
must live under `$HOME`, and a missing or malformed session file
|
|
39
|
+
resolves to `None` (unauthenticated request → upstream 401 → the chain
|
|
40
|
+
moves on) instead of raising. See `examples/providers.cli-session.yaml`.
|
|
41
|
+
- **`coderouter rollback`** — restores `providers.yaml`,
|
|
42
|
+
`~/.coderouter/model-capabilities.yaml` and (with `--workspace`)
|
|
43
|
+
`.vscode/settings.json` / `.envrc` from their `.bak` siblings.
|
|
44
|
+
`doctor --apply` and `vscode-init` have always written a backup and
|
|
45
|
+
never offered a way back. Restore is a **swap**: the current contents
|
|
46
|
+
become the new `.bak`, so a second run returns you to where you
|
|
47
|
+
started. Exit 0 restored / 2 nothing to restore / 1 a restore failed.
|
|
48
|
+
- **`coderouter doctor --check-secrets`** — proves the log-redaction
|
|
49
|
+
filter scrubs by pushing a canary through it, reports which declared
|
|
50
|
+
`api_key_env` vars are actually set, flags credentials pasted into a
|
|
51
|
+
`base_url`, and scans the already-written `requests.jsonl` /
|
|
52
|
+
`audit.jsonl` (plus rotated `.1`) for live key values. Same 0/2/1 exit
|
|
53
|
+
contract as `--check-env`. The log scan is the part that produces
|
|
54
|
+
evidence rather than reassurance.
|
|
55
|
+
- **`CODEROUTER_METRICS_TOKEN`** — opt-in token auth for `/dashboard`,
|
|
56
|
+
`/metrics.json` and `/metrics`, mirroring `CODEROUTER_LAUNCHER_TOKEN`.
|
|
57
|
+
|
|
58
|
+
### Changed (BREAKING)
|
|
59
|
+
|
|
60
|
+
- **`vscode-init --with-envrc --force` no longer replaces `.envrc`.** The
|
|
61
|
+
block CodeRouter owns is now fenced between
|
|
62
|
+
`# BEGIN coderouter-managed` / `# END coderouter-managed`, and only
|
|
63
|
+
that block is rewritten. Consequences:
|
|
64
|
+
- A re-run against a fenced file needs **no `--force`** — rewriting our
|
|
65
|
+
own fence is not destructive, so a port change is an ordinary update
|
|
66
|
+
instead of a conflict to override.
|
|
67
|
+
- `--force` now means "adopt a file I did not write": the block is
|
|
68
|
+
appended and every existing line survives. Previously it deleted
|
|
69
|
+
them, which is how the H-11 `.envrc` incident happened — including
|
|
70
|
+
the `source_env_if_exists .envrc.local` line the docs tell users to
|
|
71
|
+
add.
|
|
72
|
+
- A managed variable exported *outside* the fence is refused, because
|
|
73
|
+
direnv applies exports in order and a silent duplicate leaves nobody
|
|
74
|
+
able to say which value is live. Commented-out lines are not
|
|
75
|
+
conflicts.
|
|
76
|
+
- A file byte-identical to a pre-v2.14.0 generation is adopted into the
|
|
77
|
+
fence silently, so upgrading does not demand `--force` for our own
|
|
78
|
+
previous output.
|
|
79
|
+
`settings.json` handling is unchanged (it already refused to overwrite
|
|
80
|
+
a user-owned value without `--force`).
|
|
81
|
+
- **A failed `doctor --apply` is now a no-op instead of a partial one.**
|
|
82
|
+
The write loop had no error handling: an apply spanning `providers.yaml`
|
|
83
|
+
and `model-capabilities.yaml` left the first file rewritten when the
|
|
84
|
+
second failed. It now restores what it already wrote (and deletes what
|
|
85
|
+
it created) before raising.
|
|
86
|
+
|
|
87
|
+
### Fixed (security)
|
|
88
|
+
|
|
89
|
+
- **Credentials are scrubbed from every log sink.** CodeRouter had no
|
|
90
|
+
notion of "this string is a secret": keys were read by
|
|
91
|
+
`resolve_api_key`, dropped into `x-api-key` / `Authorization`, and
|
|
92
|
+
nothing masked them because nothing knew which strings needed masking.
|
|
93
|
+
No call site logged a header dict, so this was not a live leak — the
|
|
94
|
+
problem was that the safety of every future log line depended on
|
|
95
|
+
whoever wrote it remembering what was in their hand. There is now a
|
|
96
|
+
process-global registry keyed on the exact credential value, armed by
|
|
97
|
+
`resolve_api_key` (the choke point every key passes through, plugins
|
|
98
|
+
included) and pre-armed by `load_config`, plus a
|
|
99
|
+
`SecretRedactingFilter` on the stderr handler, `RequestLogHandler` and
|
|
100
|
+
`AuditLogHandler`. It scrubs the message, printf args, `exc_text` and
|
|
101
|
+
every `extra={...}` field, recursing into nested dicts and lists.
|
|
102
|
+
Exact-match registry first (zero false positives); a small set of
|
|
103
|
+
anchored backstop patterns (`sk-*`, `gh*_`, `AIza*`, `Bearer`,
|
|
104
|
+
`?api_key=`, URL userinfo) covers credentials that never passed through
|
|
105
|
+
the resolver. Values under 8 characters are refused — redacting those
|
|
106
|
+
would corrupt ordinary log text.
|
|
107
|
+
- **`/dashboard`, `/metrics.json` and `/metrics` were unauthenticated
|
|
108
|
+
with no way to close them.** They change nothing, so they were never
|
|
109
|
+
part of the H-8 work, but `/metrics.json` returns every provider's
|
|
110
|
+
name, kind, paid flag and `base_url` plus the profile graph — the full
|
|
111
|
+
topology of which models an operator runs and which vendors they pay.
|
|
112
|
+
With `CODEROUTER_METRICS_TOKEN` unset they stay open exactly as before
|
|
113
|
+
(no running scrape breaks on upgrade) and log a one-time
|
|
114
|
+
`metrics-auth-disabled` warning. The check runs **before** the payload
|
|
115
|
+
is assembled, so a 401 leaks nothing, and the page receives only a
|
|
116
|
+
boolean — never the token. `base_url` now also goes through the
|
|
117
|
+
scrubber on its way into the JSON, since it is the one config field an
|
|
118
|
+
operator can paste a credential into and this endpoint hands it to a
|
|
119
|
+
browser.
|
|
120
|
+
|
|
121
|
+
### Tests
|
|
122
|
+
|
|
123
|
+
- 2572 passed / 1 skipped (was 2464 on `main`), ruff clean, and 51 mypy
|
|
124
|
+
errors against 53 on `main` (net −2; strict mypy is not CI-enforced).
|
|
125
|
+
- New: `tests/test_secret_redaction.py` (36),
|
|
126
|
+
`tests/test_credentials.py` (25), `tests/test_rollback.py` (17),
|
|
127
|
+
`tests/test_ui_auth.py` (17), plus 9 in `tests/test_vscode_init.py` and
|
|
128
|
+
1 in `tests/test_doctor_apply.py`.
|
|
129
|
+
- `test_existing_envrc_force_overwrites` asserted the old destructive
|
|
130
|
+
`--force` contract and is rewritten as
|
|
131
|
+
`test_existing_envrc_force_adopts_without_destroying`, pinning the
|
|
132
|
+
opposite.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
12
136
|
## [v2.13.0] — 2026-08-07 (remaining-high security hardening)
|
|
13
137
|
|
|
14
138
|
### Changed (BREAKING)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.14.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
|
|
@@ -45,7 +45,7 @@ from coderouter.adapters.base import (
|
|
|
45
45
|
ProviderCallOverrides,
|
|
46
46
|
StreamChunk,
|
|
47
47
|
)
|
|
48
|
-
from coderouter.
|
|
48
|
+
from coderouter.credentials import resolve_provider_credential
|
|
49
49
|
from coderouter.logging import get_logger, log_output_filter_applied
|
|
50
50
|
from coderouter.output_filters import OutputFilterChain
|
|
51
51
|
from coderouter.translation.anthropic import (
|
|
@@ -134,7 +134,9 @@ class AnthropicAdapter(BaseAdapter):
|
|
|
134
134
|
self.config.extra_body.get("anthropic_version", _DEFAULT_ANTHROPIC_VERSION)
|
|
135
135
|
),
|
|
136
136
|
}
|
|
137
|
-
|
|
137
|
+
# v2.14.0: see openai_compat._headers — same resolver, so an
|
|
138
|
+
# Anthropic-protocol provider can also ride a CLI session token.
|
|
139
|
+
api_key = resolve_provider_credential(self.config)
|
|
138
140
|
if api_key:
|
|
139
141
|
headers["x-api-key"] = api_key
|
|
140
142
|
# v0.4-D: forward the client's anthropic-beta header verbatim.
|
|
@@ -29,7 +29,7 @@ from coderouter.adapters.base import (
|
|
|
29
29
|
ProviderCallOverrides,
|
|
30
30
|
StreamChunk,
|
|
31
31
|
)
|
|
32
|
-
from coderouter.
|
|
32
|
+
from coderouter.credentials import resolve_provider_credential
|
|
33
33
|
from coderouter.logging import (
|
|
34
34
|
get_logger,
|
|
35
35
|
log_capability_degraded,
|
|
@@ -105,7 +105,10 @@ class OpenAICompatAdapter(BaseAdapter):
|
|
|
105
105
|
"Content-Type": "application/json",
|
|
106
106
|
"User-Agent": "CodeRouter/0.1",
|
|
107
107
|
}
|
|
108
|
-
|
|
108
|
+
# v2.14.0: resolve_provider_credential covers api_key_env (the
|
|
109
|
+
# historical path, unchanged) and credential.source=cli_session,
|
|
110
|
+
# which borrows the token a vendor CLI already wrote to disk.
|
|
111
|
+
api_key = resolve_provider_credential(self.config)
|
|
109
112
|
if api_key:
|
|
110
113
|
headers["Authorization"] = f"Bearer {api_key}"
|
|
111
114
|
return headers
|
|
@@ -149,6 +149,22 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
149
149
|
"See docs/guides/troubleshooting.md §5 for the threat model."
|
|
150
150
|
),
|
|
151
151
|
)
|
|
152
|
+
# v2.14.0: --check-secrets is the runtime counterpart to --check-env.
|
|
153
|
+
# --check-env asks "is the file holding my keys protected?"; this asks
|
|
154
|
+
# "does the running process know its own secrets, and did any of them
|
|
155
|
+
# already reach a log file?". The second question is the one that used
|
|
156
|
+
# to have no answer at all.
|
|
157
|
+
doctor.add_argument(
|
|
158
|
+
"--check-secrets",
|
|
159
|
+
action="store_true",
|
|
160
|
+
help=(
|
|
161
|
+
"Audit credential hygiene: prove the log-redaction filter "
|
|
162
|
+
"actually scrubs, report which declared api_key_env vars are "
|
|
163
|
+
"set, flag credentials pasted into a base_url, and scan the "
|
|
164
|
+
"already-written requests.jsonl / audit.jsonl under state_dir "
|
|
165
|
+
"for live key values. Exit 0 clean / 2 needs attention / 1 leak."
|
|
166
|
+
),
|
|
167
|
+
)
|
|
152
168
|
doctor.add_argument(
|
|
153
169
|
"--config",
|
|
154
170
|
default=None,
|
|
@@ -192,6 +208,55 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
192
208
|
),
|
|
193
209
|
)
|
|
194
210
|
|
|
211
|
+
# v2.14.0: `coderouter rollback` — the missing half of --apply.
|
|
212
|
+
# Both writers (doctor --apply, vscode-init) already dropped a `.bak`
|
|
213
|
+
# next to every file they rewrote; nothing could put it back. Restore
|
|
214
|
+
# is a swap, not an overwrite, so running it twice returns you to
|
|
215
|
+
# where you started rather than destroying the newer version.
|
|
216
|
+
rollback = sub.add_parser(
|
|
217
|
+
"rollback",
|
|
218
|
+
help="Restore files a previous --apply / vscode-init rewrote (v2.14.0).",
|
|
219
|
+
description=(
|
|
220
|
+
"Swap each managed file with its .bak sibling: providers.yaml, "
|
|
221
|
+
"~/.coderouter/model-capabilities.yaml, and (with --workspace) "
|
|
222
|
+
".vscode/settings.json and .envrc. The current contents become "
|
|
223
|
+
"the new .bak, so a second run toggles back. "
|
|
224
|
+
"Exit codes: 0 restored, 2 nothing to restore, 1 a restore failed."
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
rollback.add_argument(
|
|
228
|
+
"--config",
|
|
229
|
+
default=None,
|
|
230
|
+
help=(
|
|
231
|
+
"Path to providers.yaml. Defaults to the same file the loader "
|
|
232
|
+
"would read ($CODEROUTER_CONFIG, then ~/.coderouter/providers.yaml)."
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
rollback.add_argument(
|
|
236
|
+
"--workspace",
|
|
237
|
+
metavar="DIR",
|
|
238
|
+
default=None,
|
|
239
|
+
help=(
|
|
240
|
+
"Also restore the vscode-init outputs under DIR: "
|
|
241
|
+
"DIR/.vscode/settings.json and DIR/.envrc."
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
rollback.add_argument(
|
|
245
|
+
"--path",
|
|
246
|
+
action="append",
|
|
247
|
+
metavar="FILE",
|
|
248
|
+
default=None,
|
|
249
|
+
help=(
|
|
250
|
+
"Restore exactly this file from its .bak sibling. Repeatable. "
|
|
251
|
+
"When given, the default discovery set is not used."
|
|
252
|
+
),
|
|
253
|
+
)
|
|
254
|
+
rollback.add_argument(
|
|
255
|
+
"--dry-run",
|
|
256
|
+
action="store_true",
|
|
257
|
+
help="Report what would be restored without touching any file.",
|
|
258
|
+
)
|
|
259
|
+
|
|
195
260
|
# v1.5-C: `coderouter stats` — live TUI over GET /metrics.json.
|
|
196
261
|
# Lazy-imports ``curses`` inside the runner so the CLI boot stays
|
|
197
262
|
# snappy and environments without curses (rare, but e.g. minimal
|
|
@@ -490,6 +555,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
490
555
|
if args.command == "doctor":
|
|
491
556
|
return _run_doctor(args)
|
|
492
557
|
|
|
558
|
+
if args.command == "rollback":
|
|
559
|
+
return _run_rollback(args)
|
|
560
|
+
|
|
493
561
|
if args.command == "stats":
|
|
494
562
|
# v1.5-C: stats is intentionally a thin wrapper — all logic
|
|
495
563
|
# (fetch, render, curses loop) lives in coderouter.cli_stats so
|
|
@@ -526,9 +594,11 @@ def _run_doctor(args: argparse.Namespace) -> int:
|
|
|
526
594
|
worst-case of the two reports so CI guarding against both leak
|
|
527
595
|
risks AND broken providers can use a single command.
|
|
528
596
|
"""
|
|
529
|
-
|
|
597
|
+
check_secrets = bool(getattr(args, "check_secrets", False))
|
|
598
|
+
if args.check_model is None and args.check_env is None and not check_secrets:
|
|
530
599
|
print(
|
|
531
|
-
"doctor: provide --check-model PROVIDER
|
|
600
|
+
"doctor: provide --check-model PROVIDER, --check-env [PATH], "
|
|
601
|
+
"and/or --check-secrets",
|
|
532
602
|
file=sys.stderr,
|
|
533
603
|
)
|
|
534
604
|
return 1
|
|
@@ -541,6 +611,9 @@ def _run_doctor(args: argparse.Namespace) -> int:
|
|
|
541
611
|
if args.check_env is not None:
|
|
542
612
|
worst_exit = max(worst_exit, _run_check_env(args.check_env))
|
|
543
613
|
|
|
614
|
+
if check_secrets:
|
|
615
|
+
worst_exit = max(worst_exit, _run_check_secrets(args.config))
|
|
616
|
+
|
|
544
617
|
if args.check_model is not None:
|
|
545
618
|
worst_exit = max(worst_exit, _run_check_model(args))
|
|
546
619
|
|
|
@@ -969,5 +1042,70 @@ def _run_check_env(arg_value: str) -> int:
|
|
|
969
1042
|
return exit_code_for_env_security(report)
|
|
970
1043
|
|
|
971
1044
|
|
|
1045
|
+
def _run_rollback(args: argparse.Namespace) -> int:
|
|
1046
|
+
"""v2.14.0: put back what ``--apply`` / ``vscode-init`` overwrote.
|
|
1047
|
+
|
|
1048
|
+
Explicit ``--path`` wins over discovery so an operator can restore one
|
|
1049
|
+
file without also reverting an unrelated ``model-capabilities.yaml``
|
|
1050
|
+
edit from last week. Discovery resolves providers.yaml through the
|
|
1051
|
+
loader's own search order rather than a second copy of it.
|
|
1052
|
+
"""
|
|
1053
|
+
from coderouter.rollback import (
|
|
1054
|
+
discover_managed_files,
|
|
1055
|
+
exit_code_for_rollback,
|
|
1056
|
+
format_rollback_report,
|
|
1057
|
+
restore_many,
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
explicit = getattr(args, "path", None)
|
|
1061
|
+
if explicit:
|
|
1062
|
+
targets: list[Path] = [Path(p).expanduser() for p in explicit]
|
|
1063
|
+
else:
|
|
1064
|
+
try:
|
|
1065
|
+
config_path: Path | None = _resolve_config_path(args.config)
|
|
1066
|
+
except Exception:
|
|
1067
|
+
# No config anywhere is not fatal here — the workspace files
|
|
1068
|
+
# and the user-layer capabilities file may still have backups.
|
|
1069
|
+
config_path = None
|
|
1070
|
+
targets = discover_managed_files(
|
|
1071
|
+
config_path=config_path,
|
|
1072
|
+
workspace=getattr(args, "workspace", None),
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
outcomes = restore_many(targets, dry_run=bool(args.dry_run))
|
|
1076
|
+
print(format_rollback_report(outcomes, dry_run=bool(args.dry_run)))
|
|
1077
|
+
return exit_code_for_rollback(outcomes)
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def _run_check_secrets(config_path: str | None) -> int:
|
|
1081
|
+
"""v2.14.0: credential-hygiene audit for the running configuration.
|
|
1082
|
+
|
|
1083
|
+
Loads the config through the normal loader — which is also what arms
|
|
1084
|
+
the redaction registry — then runs the suite from
|
|
1085
|
+
:mod:`coderouter.secret_redaction`. A config that fails to load is a
|
|
1086
|
+
blocker (exit 1) rather than a skip: we cannot claim anything about
|
|
1087
|
+
secret hygiene for a file we could not read.
|
|
1088
|
+
"""
|
|
1089
|
+
from coderouter.config.loader import load_config
|
|
1090
|
+
from coderouter.secret_redaction import (
|
|
1091
|
+
check_secret_hygiene,
|
|
1092
|
+
exit_code_for_secret_report,
|
|
1093
|
+
format_secret_report,
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
try:
|
|
1097
|
+
config = load_config(config_path)
|
|
1098
|
+
except FileNotFoundError as exc:
|
|
1099
|
+
print(f"doctor: {exc}", file=sys.stderr)
|
|
1100
|
+
return 1
|
|
1101
|
+
except Exception as exc:
|
|
1102
|
+
print(f"doctor: failed to load config: {exc}", file=sys.stderr)
|
|
1103
|
+
return 1
|
|
1104
|
+
|
|
1105
|
+
report = check_secret_hygiene(config)
|
|
1106
|
+
print(format_secret_report(report))
|
|
1107
|
+
return exit_code_for_secret_report(report)
|
|
1108
|
+
|
|
1109
|
+
|
|
972
1110
|
if __name__ == "__main__":
|
|
973
1111
|
sys.exit(main())
|
|
@@ -34,6 +34,7 @@ import yaml
|
|
|
34
34
|
|
|
35
35
|
from coderouter.config.schemas import CodeRouterConfig
|
|
36
36
|
from coderouter.logging import get_logger
|
|
37
|
+
from coderouter.secret_redaction import register_config_secrets, register_secret
|
|
37
38
|
|
|
38
39
|
logger = get_logger(__name__)
|
|
39
40
|
|
|
@@ -253,12 +254,34 @@ def load_config(path: str | os.PathLike[str] | None = None) -> CodeRouterConfig:
|
|
|
253
254
|
elif env_paid in {"0", "false", "no", "off"}:
|
|
254
255
|
config.allow_paid = False
|
|
255
256
|
|
|
257
|
+
# v2.14.0: arm log redaction before anything else runs. Registering here
|
|
258
|
+
# (rather than lazily on the first resolve_api_key call) means the
|
|
259
|
+
# startup log lines — which name providers and profiles — are already
|
|
260
|
+
# scrubbed, and so is any config-validation error raised downstream.
|
|
261
|
+
# Only labels are logged; the values never leave the registry.
|
|
262
|
+
registered = register_config_secrets(config)
|
|
263
|
+
if registered:
|
|
264
|
+
logger.info(
|
|
265
|
+
"secret-redaction-armed",
|
|
266
|
+
extra={"count": len(registered), "labels": sorted(set(registered))},
|
|
267
|
+
)
|
|
268
|
+
|
|
256
269
|
return config
|
|
257
270
|
|
|
258
271
|
|
|
259
272
|
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.
|
|
273
|
+
"""Look up an API key from the named env var. Returns None if unset.
|
|
274
|
+
|
|
275
|
+
v2.14.0: the resolved value is registered with
|
|
276
|
+
:mod:`coderouter.secret_redaction` before it is handed out. This is the
|
|
277
|
+
moment a credential enters the process, so it is the one place that can
|
|
278
|
+
guarantee the log scrubber knows about every key we actually use —
|
|
279
|
+
including keys for providers added by an adapter plugin, which never
|
|
280
|
+
pass through ``register_config_secrets``.
|
|
281
|
+
"""
|
|
261
282
|
if not api_key_env:
|
|
262
283
|
return None
|
|
263
284
|
value = os.environ.get(api_key_env, "").strip()
|
|
285
|
+
if value:
|
|
286
|
+
register_secret(value, api_key_env)
|
|
264
287
|
return value or None
|
|
@@ -371,6 +371,120 @@ RESTART_COMMAND_SHELL_META_RE = re.compile(r"(&&|\|\||\||;|>|<|\$\(|`)")
|
|
|
371
371
|
RESTART_COMMAND_ENV_PREFIX_RE = re.compile(r"^\w+=")
|
|
372
372
|
|
|
373
373
|
|
|
374
|
+
class CredentialRefresh(BaseModel):
|
|
375
|
+
"""How to make a vendor CLI rotate the token it wrote to disk.
|
|
376
|
+
|
|
377
|
+
Deliberately not an OAuth client. Every vendor has its own endpoint,
|
|
378
|
+
client id, rotation policy and error shape, and all of them change —
|
|
379
|
+
delegating to the CLI that owns the file is the version that survives
|
|
380
|
+
contact with a moving vendor.
|
|
381
|
+
|
|
382
|
+
``command`` is an argv **list**, dispatched with ``shell=False``. Same
|
|
383
|
+
trust decision as v2.13.0's ``restart_command``: a string that goes
|
|
384
|
+
through a shell turns a config file into arbitrary code execution.
|
|
385
|
+
There is no string form here to have to refuse.
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
model_config = ConfigDict(extra="forbid")
|
|
389
|
+
|
|
390
|
+
command: list[str] = Field(
|
|
391
|
+
...,
|
|
392
|
+
min_length=1,
|
|
393
|
+
description=(
|
|
394
|
+
"argv for the vendor CLI, e.g. ['grok', 'models']. Run with "
|
|
395
|
+
"shell=False; no shell metacharacters are interpreted."
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
early_ratio: float = Field(
|
|
399
|
+
default=0.5,
|
|
400
|
+
ge=0.0,
|
|
401
|
+
le=1.0,
|
|
402
|
+
description=(
|
|
403
|
+
"Refresh when the remaining lifetime drops below this fraction "
|
|
404
|
+
"of itself. Early refresh avoids a token dying mid-request."
|
|
405
|
+
),
|
|
406
|
+
)
|
|
407
|
+
min_lead_s: float = Field(
|
|
408
|
+
default=300.0,
|
|
409
|
+
ge=0.0,
|
|
410
|
+
description="Always refresh at least this many seconds before expiry.",
|
|
411
|
+
)
|
|
412
|
+
timeout_s: float = Field(
|
|
413
|
+
default=30.0,
|
|
414
|
+
gt=0.0,
|
|
415
|
+
le=600.0,
|
|
416
|
+
description="Hard timeout for the refresh command.",
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class ProviderCredential(BaseModel):
|
|
421
|
+
"""Where this provider's credential comes from (v2.14.0).
|
|
422
|
+
|
|
423
|
+
``source: env`` is the historical behaviour, spelled out. ``source:
|
|
424
|
+
cli_session`` borrows the token a vendor CLI already wrote to disk, so
|
|
425
|
+
a subscription-authenticated provider can be an ordinary
|
|
426
|
+
``openai_compat`` entry instead of a ``kind: agent_cli`` island — and
|
|
427
|
+
therefore takes part in fallback chains, auto-routing, the budget
|
|
428
|
+
tracker and every other routing feature.
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
model_config = ConfigDict(extra="forbid")
|
|
432
|
+
|
|
433
|
+
source: Literal["env", "cli_session"] = Field(
|
|
434
|
+
default="env",
|
|
435
|
+
description="'env' reads an environment variable; 'cli_session' reads a JSON file.",
|
|
436
|
+
)
|
|
437
|
+
env: str | None = Field(
|
|
438
|
+
default=None, description="source=env: the environment variable name."
|
|
439
|
+
)
|
|
440
|
+
path: str | None = Field(
|
|
441
|
+
default=None,
|
|
442
|
+
description=(
|
|
443
|
+
"source=cli_session: the JSON file the vendor CLI writes, e.g. "
|
|
444
|
+
"~/.kimi-code/credentials/kimi-code.json. Must live under $HOME."
|
|
445
|
+
),
|
|
446
|
+
)
|
|
447
|
+
field: str = Field(
|
|
448
|
+
default="access_token",
|
|
449
|
+
description="Dotted path to the token inside that JSON (e.g. 'tokens.access').",
|
|
450
|
+
)
|
|
451
|
+
expiry_field: str = Field(
|
|
452
|
+
default="expires_at",
|
|
453
|
+
description=(
|
|
454
|
+
"Dotted path to the expiry. Epoch seconds or milliseconds; both "
|
|
455
|
+
"are accepted. Absent means 'no expiry info' — the upstream 401 "
|
|
456
|
+
"becomes the signal instead."
|
|
457
|
+
),
|
|
458
|
+
)
|
|
459
|
+
refresh: CredentialRefresh | None = Field(
|
|
460
|
+
default=None,
|
|
461
|
+
description="Omit to never refresh (a long-lived token, or one you rotate yourself).",
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
@model_validator(mode="after")
|
|
465
|
+
def _check_source_requirements(self) -> Self:
|
|
466
|
+
"""Each source needs its own field, and only its own field."""
|
|
467
|
+
if self.source == "env":
|
|
468
|
+
if not self.env:
|
|
469
|
+
raise ValueError("credential.source='env' requires credential.env")
|
|
470
|
+
if self.path:
|
|
471
|
+
raise ValueError("credential.path is meaningless for source='env'")
|
|
472
|
+
if self.source == "cli_session":
|
|
473
|
+
if not self.path:
|
|
474
|
+
raise ValueError(
|
|
475
|
+
"credential.source='cli_session' requires credential.path"
|
|
476
|
+
)
|
|
477
|
+
if self.env:
|
|
478
|
+
raise ValueError("credential.env is meaningless for source='cli_session'")
|
|
479
|
+
from coderouter.credentials import session_path_is_sane
|
|
480
|
+
|
|
481
|
+
if not session_path_is_sane(self.path):
|
|
482
|
+
raise ValueError(
|
|
483
|
+
f"credential.path must live under your home directory: {self.path}"
|
|
484
|
+
)
|
|
485
|
+
return self
|
|
486
|
+
|
|
487
|
+
|
|
374
488
|
class ProviderConfig(BaseModel):
|
|
375
489
|
"""A single provider entry from providers.yaml.
|
|
376
490
|
|
|
@@ -416,6 +530,19 @@ class ProviderConfig(BaseModel):
|
|
|
416
530
|
default=None,
|
|
417
531
|
description="Env var name holding the API key. None = no auth (e.g. local).",
|
|
418
532
|
)
|
|
533
|
+
# v2.14.0: the general form of the above. Mutually exclusive with
|
|
534
|
+
# api_key_env — a resolver that silently fell back between sources would
|
|
535
|
+
# make "which credential is this request actually using?" unanswerable,
|
|
536
|
+
# which is the question an operator asks at exactly the wrong moment.
|
|
537
|
+
credential: ProviderCredential | None = Field(
|
|
538
|
+
default=None,
|
|
539
|
+
description=(
|
|
540
|
+
"Where the credential comes from. Omit to use api_key_env. "
|
|
541
|
+
"source='cli_session' reads the token a vendor CLI already wrote "
|
|
542
|
+
"to disk, so a subscription provider can be a plain "
|
|
543
|
+
"openai_compat entry that takes part in fallback chains."
|
|
544
|
+
),
|
|
545
|
+
)
|
|
419
546
|
|
|
420
547
|
# Routing-relevant flags
|
|
421
548
|
paid: bool = Field(
|
|
@@ -611,6 +738,23 @@ class ProviderConfig(BaseModel):
|
|
|
611
738
|
validate_output_filters(self.output_filters)
|
|
612
739
|
return self
|
|
613
740
|
|
|
741
|
+
@model_validator(mode="after")
|
|
742
|
+
def _check_credential_exclusive(self) -> Self:
|
|
743
|
+
"""``api_key_env`` and ``credential`` cannot both be set.
|
|
744
|
+
|
|
745
|
+
Failing at load time is the whole point: two credential sources on
|
|
746
|
+
one provider is a question ("which one won?") that should never
|
|
747
|
+
reach a request, let alone a log line an operator has to reverse
|
|
748
|
+
engineer at 2am.
|
|
749
|
+
"""
|
|
750
|
+
if self.credential is not None and self.api_key_env:
|
|
751
|
+
raise ValueError(
|
|
752
|
+
f"provider {self.name!r}: set either api_key_env or credential, "
|
|
753
|
+
"not both (credential.source='env' with credential.env=... is "
|
|
754
|
+
"the spelled-out form of api_key_env)"
|
|
755
|
+
)
|
|
756
|
+
return self
|
|
757
|
+
|
|
614
758
|
@model_validator(mode="after")
|
|
615
759
|
def _check_kind_requirements(self) -> ProviderConfig:
|
|
616
760
|
"""Enforce per-``kind`` field requirements (external-agents design §5.2.2).
|