coderouter-cli 2.15.1__tar.gz → 2.17.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.15.1 → coderouter_cli-2.17.0}/CHANGELOG.md +121 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/PKG-INFO +1 -1
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/adapters/anthropic_native.py +4 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/adapters/base.py +37 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/adapters/openai_compat.py +4 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/cli.py +67 -2
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/config/schemas.py +58 -2
- coderouter_cli-2.17.0/coderouter/doctor_claude_code.py +181 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/guards/context_budget.py +19 -0
- coderouter_cli-2.17.0/coderouter/ingress/anthropic_errors.py +204 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/ingress/anthropic_routes.py +221 -8
- coderouter_cli-2.17.0/coderouter/ingress/tool_search.py +100 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/routing/fallback.py +104 -6
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/routing/fallback_trace.py +8 -1
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/vscode_init.py +56 -6
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/README.en.md +1 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/README.md +1 -0
- coderouter_cli-2.17.0/docs/concepts/commit-point.en.md +68 -0
- coderouter_cli-2.17.0/docs/concepts/commit-point.md +70 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/concepts/context-budget.en.md +7 -6
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/concepts/context-budget.md +7 -6
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/vscode.md +12 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/start/quickstart.en.md +10 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/start/quickstart.md +7 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/pyproject.toml +1 -1
- coderouter_cli-2.17.0/tests/test_anthropic_errors.py +381 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_context_budget.py +55 -0
- coderouter_cli-2.17.0/tests/test_doctor_claude_code.py +79 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_ingress_anthropic.py +15 -17
- coderouter_cli-2.17.0/tests/test_tool_search.py +137 -0
- coderouter_cli-2.17.0/tests/test_ttft_timeout.py +237 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_vscode_init.py +80 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/.gitignore +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/LICENSE +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/README.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/README.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/config/loader.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/cost.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/credentials.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/errors.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/guards/self_healing.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/ingress/launcher_routes.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/ingress/openai_routes.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/ingress/ui_auth.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/launcher_devices.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/launcher_speculative.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/launcher_swap.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/logging.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/py.typed +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/rollback.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/routing/capability.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/secret_redaction.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/state/audit_log.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/state/request_log.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/translation/convert.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/external-agents.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/external-agents.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/gguf_dl.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/hf-ollama-models.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/launcher-quickstart.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/launcher.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/launcher.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/llamacpp-direct.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/llamacpp-direct.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/concepts/architecture.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/concepts/continuous-probing.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/concepts/drift-detection.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/concepts/partial-stitch.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/concepts/stream-truncation.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/concepts/stream-truncation.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/designs/agent-cli-plugin-extraction.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/designs/external-agents-adapter.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/designs/launcher-model-swap.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/designs/launcher-multi-build.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/designs/orchestration-companion.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/remote-access.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/remote-access.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/security.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/subagent-routing.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/subagent-routing.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/low-memory-integration.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/.env.example +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/README.md +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/providers-agent-cli.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/providers-multiagent.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/providers.cli-session.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/providers.llamacpp-vllm.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/providers.opusplan.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/providers.raspberrypi.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/providers.swap.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/examples/providers.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/__init__.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/conftest.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_agent_cli.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_auto_router.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_budget.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_capability.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_capability_tool_choice.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_cli.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_cli_bind_warning.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_config.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_config_cwd_warning.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_config_shim_actions.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_credentials.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_errors.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_examples_yaml_roundtrip.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fallback_cache_strip.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fallback_empty_response.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fallback_reason.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fallback_tool_choice_emulate.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fallback_tool_integrity.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fix_h1_prometheus_drift.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fix_h2_h6_translation.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fix_h3_client_reuse.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fix_h4_h5_guards.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fix_h7_probe_restore.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fix_h8_launcher_auth.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fix_mA_routing.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fix_mB_adapters.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fix_mC_convert.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fix_mD_logging_metrics.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fix_mE_config_validation.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fix_mF_ingress.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_fix_v213_launcher_hardening.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_import_hygiene.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_ingress_count_tokens.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_backend_variants.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_config_bench.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_devices.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_devices_detect.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_devices_routes.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_gui_devices.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_gui_events.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_gui_readiness.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_gui_sweep_close.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_mtp.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_mtp_fallback.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_provider_sync.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_readiness_restart.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_swap.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_swap_review.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_variant_config.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_variant_gui.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_variant_routes.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_launcher_variant_sweep.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_models_passthrough.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_packaging.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_plugin_adapter.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_rollback.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_secret_redaction.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_stop_reason_forward_compat.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_stream_truncation.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_tool_repair_differential.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_tool_repair_lenient.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_tool_repair_r4.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_tool_repair_scanners.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_toolrepair_bench.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_translation_reverse.py +0 -0
- {coderouter_cli-2.15.1 → coderouter_cli-2.17.0}/tests/test_ui_auth.py +0 -0
|
@@ -9,6 +9,127 @@ are kept verbatim where the Japanese text itself is the subject).
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## [v2.17.0] — 2026-10-24 (time-to-first-token cut-off, retry signals)
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`ttft_timeout_s` (provider).** A streaming attempt that has not
|
|
17
|
+
produced its first real output (text / tool_use / thinking delta) within
|
|
18
|
+
this many seconds is abandoned before any byte reached the client, and
|
|
19
|
+
the chain moves on — e.g. a local llama-server still loading a model or
|
|
20
|
+
prefilling a long prompt while a cloud provider could answer now. Never
|
|
21
|
+
applied to the last provider of a chain (nothing to fall back to), nor to
|
|
22
|
+
non-streaming requests (`timeout_s` covers those), nor to the
|
|
23
|
+
`/v1/chat/completions` route. It is also skipped for the tool-turn
|
|
24
|
+
downgrade (`kind: openai_compat` with tools, which calls the upstream
|
|
25
|
+
non-streaming so tool calls can be repaired): there the "first output" is
|
|
26
|
+
the whole answer, and a deadline would discard slow but healthy replies —
|
|
27
|
+
use `kind: anthropic` (native `/v1/messages`) to get the cut-off for a
|
|
28
|
+
local server. It always covers the
|
|
29
|
+
wait for the first event; covering the gap between the opening events and
|
|
30
|
+
the first real output needs `empty_response_action: fallback`, which is
|
|
31
|
+
what withholds those events from the client. Abandoned attempts are
|
|
32
|
+
ordinary retryable failures: new fallback reason `ttft-timeout` (detail
|
|
33
|
+
`no-first-token`) and the self-healing guards learn from them. A cut that
|
|
34
|
+
happens after the opening events is handled like a truncated stream: the
|
|
35
|
+
withheld, unterminated preamble is never flushed to the client if the
|
|
36
|
+
rest of the chain also fails, and the abandoned upstream stream is closed
|
|
37
|
+
immediately. Default
|
|
38
|
+
`None` (off). Docs: `docs/concepts/commit-point.md` / `.en.md`.
|
|
39
|
+
- **`x-should-retry` on `/v1/messages` errors.** Every Anthropic-shaped
|
|
40
|
+
error CodeRouter produces now says whether retrying can help — the header
|
|
41
|
+
Claude Code reads, with `retry-after`, to decide whether to spend its
|
|
42
|
+
retry budget: `false` for 400 / 413 (including tool-search and
|
|
43
|
+
`context_budget_action: reject` refusals), `true` for 429, and for 502
|
|
44
|
+
`true` only when at least one upstream failure was retryable (a chain that
|
|
45
|
+
failed purely on 401/403 fails the same way on every retry).
|
|
46
|
+
- **Docs: the commit point** (`docs/concepts/commit-point.md` / `.en.md`) —
|
|
47
|
+
which failures before the body ships CodeRouter can still move to another
|
|
48
|
+
provider, which it cannot, and the error each chain failure becomes.
|
|
49
|
+
Recommends `empty_response_action: fallback` for Claude Code profiles.
|
|
50
|
+
|
|
51
|
+
### Known limits
|
|
52
|
+
|
|
53
|
+
- No keep-alive `ping` is sent while opening events are withheld; keep
|
|
54
|
+
`ttft_timeout_s` below the client's idle timeout
|
|
55
|
+
(`CLAUDE_STREAM_IDLE_TIMEOUT_MS` for Claude Code).
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## [v2.16.0] — 2026-10-24 (Claude Code 2.1.3xx behind a gateway)
|
|
60
|
+
|
|
61
|
+
Claude Code 2.1.285–2.1.309 changed several defaults for sessions behind a
|
|
62
|
+
custom `ANTHROPIC_BASE_URL`: tool search is on, Opus 4.7+ / Sonnet 5+ /
|
|
63
|
+
Fable ids are treated as 1M-context models, auto mode asks the gateway for a
|
|
64
|
+
server-side classifier, and recovery from rejected features or an
|
|
65
|
+
overflowing prompt now depends on reading the gateway's error message. This
|
|
66
|
+
release makes CodeRouter answer in the shape those recoveries expect.
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
|
|
70
|
+
- **Anthropic-shaped errors when the whole chain fails (`/v1/messages`).**
|
|
71
|
+
A failed chain used to come back as FastAPI's `{"detail": "..."}` with
|
|
72
|
+
HTTP 502 whatever the upstreams said. It now uses Anthropic's envelope
|
|
73
|
+
(`{"type": "error", "error": {"type", "message"}}`) and a status chosen
|
|
74
|
+
from what the upstreams said (`coderouter/ingress/anthropic_errors.py`):
|
|
75
|
+
- an upstream context-overflow error (llama.cpp
|
|
76
|
+
`exceed_context_size_error`, vLLM / OpenAI "maximum context length",
|
|
77
|
+
Ollama "prompt too long", …) → **400 `invalid_request_error`, message
|
|
78
|
+
starting `prompt is too long`**, which is what triggers Claude Code's
|
|
79
|
+
reactive compaction. An overflow caused by the *completion* budget
|
|
80
|
+
("… 32000 in the completion", `max_tokens`) is not reported this way —
|
|
81
|
+
compacting the prompt cannot fix it — and goes out as a plain 400;
|
|
82
|
+
- a non-retryable upstream 400 / 422 → 400 with the upstream's own
|
|
83
|
+
message (Claude Code drops a rejected beta header / feature only when it
|
|
84
|
+
can read why);
|
|
85
|
+
- 413 → `request_too_large`; 429 → `rate_limit_error` with the upstream's
|
|
86
|
+
`retry-after` passed through;
|
|
87
|
+
- upstream 401 / 403 and everything else → 502 `api_error` (they concern
|
|
88
|
+
CodeRouter's own upstream credentials, not the client's).
|
|
89
|
+
Upstream messages are secret-redacted and length-capped. `AdapterError`
|
|
90
|
+
now keeps the upstream body (capped at 4096 chars) and `Retry-After`.
|
|
91
|
+
- **Streaming requests get a real HTTP status for pre-content failures.**
|
|
92
|
+
The route now pulls the engine's first event before committing the
|
|
93
|
+
response, so a chain that fails before producing anything is answered
|
|
94
|
+
with the status above instead of `200` plus an in-stream `error` event —
|
|
95
|
+
as api.anthropic.com does. Once the first event exists, nothing changes.
|
|
96
|
+
The wait shares the stream's overall M14 ceiling (one deadline, not two),
|
|
97
|
+
and a client that disconnects during it cancels the upstream attempt.
|
|
98
|
+
- **`deferred_tools_action` (profile; `auto` | `reject` | `expand` |
|
|
99
|
+
`passthrough`, default `auto`).** Claude Code 2.1.304+ sends a server-side
|
|
100
|
+
`tool_search_tool_*` and tools marked `defer_loading: true`; only
|
|
101
|
+
api.anthropic.com runs the search. `auto` passes the request through only
|
|
102
|
+
when every provider in the chain is `kind: anthropic` on api.anthropic.com
|
|
103
|
+
and otherwise answers 400, which makes Claude Code (2.1.305+) retry
|
|
104
|
+
without tool search. `expand` drops the search tool and the flags and
|
|
105
|
+
sends every tool up front.
|
|
106
|
+
- **`context_budget_action: reject`.** At `context_budget_trim_threshold`
|
|
107
|
+
the guard answers 400 `prompt is too long: ~N tokens > M maximum` instead
|
|
108
|
+
of trimming, so Claude Code summarizes the conversation itself rather
|
|
109
|
+
than losing history silently. Works for streaming and non-streaming.
|
|
110
|
+
- **`coderouter vscode-init` writes Claude Code gateway settings** by
|
|
111
|
+
default: `ENABLE_TOOL_SEARCH=false`, `CLAUDE_CODE_AUTO_MODE_SERVER=0`,
|
|
112
|
+
and — with the new `--max-context-tokens N` —
|
|
113
|
+
`CLAUDE_CODE_MAX_CONTEXT_TOKENS=N`. `--no-claude-code-env` opts out. Same
|
|
114
|
+
merge / conflict rules as the existing variables; an export of one of
|
|
115
|
+
them outside the `.envrc` fence conflicts only when this run writes it.
|
|
116
|
+
- **`coderouter doctor --check-claude-code`.** Local-only check of the
|
|
117
|
+
Claude Code install: warns on versions with known gateway bugs (before
|
|
118
|
+
2.1.301: compaction can repeat; 2.1.304: every request fails when the
|
|
119
|
+
gateway rejects tool search) and on missing `ENABLE_TOOL_SEARCH`,
|
|
120
|
+
`CLAUDE_CODE_MAX_CONTEXT_TOKENS` (2.1.285+) and
|
|
121
|
+
`CLAUDE_CODE_AUTO_MODE_SERVER`. Exit 0 OK / 2 needs attention.
|
|
122
|
+
|
|
123
|
+
### Changed
|
|
124
|
+
|
|
125
|
+
- **Breaking for clients that parse error bodies:** `/v1/messages` errors
|
|
126
|
+
for a failed chain are no longer `{"detail": "..."}`, and a streaming
|
|
127
|
+
request whose chain fails before the first event is no longer
|
|
128
|
+
`200` + `event: error`. Both now match the Anthropic API. The
|
|
129
|
+
`/v1/chat/completions` (OpenAI) route is unchanged.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
12
133
|
## [v2.15.1] — 2026-10-24 (docs and defaults for the October 2026 ecosystem)
|
|
13
134
|
|
|
14
135
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.17.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
|
|
@@ -458,6 +458,8 @@ class AnthropicAdapter(BaseAdapter):
|
|
|
458
458
|
provider=self.name,
|
|
459
459
|
status_code=resp.status_code,
|
|
460
460
|
retryable=resp.status_code in _RETRYABLE_STATUSES,
|
|
461
|
+
upstream_body=resp.text,
|
|
462
|
+
retry_after=resp.headers.get("retry-after"),
|
|
461
463
|
)
|
|
462
464
|
|
|
463
465
|
try:
|
|
@@ -570,6 +572,8 @@ class AnthropicAdapter(BaseAdapter):
|
|
|
570
572
|
provider=self.name,
|
|
571
573
|
status_code=resp.status_code,
|
|
572
574
|
retryable=resp.status_code in _RETRYABLE_STATUSES,
|
|
575
|
+
upstream_body=body.decode("utf-8", errors="replace"),
|
|
576
|
+
retry_after=resp.headers.get("retry-after"),
|
|
573
577
|
)
|
|
574
578
|
|
|
575
579
|
# Anthropic SSE block shape:
|
|
@@ -101,6 +101,12 @@ class StreamChunk(BaseModel):
|
|
|
101
101
|
usage: dict[str, Any] | None = None
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
#: v2.16.0: max chars of an upstream error body an :class:`AdapterError`
|
|
105
|
+
#: keeps. Error bodies are small; the cap only bounds a misbehaving upstream
|
|
106
|
+
#: that answers an error status with a large HTML page.
|
|
107
|
+
UPSTREAM_BODY_CAP = 4096
|
|
108
|
+
|
|
109
|
+
|
|
104
110
|
class AdapterError(CodeRouterError):
|
|
105
111
|
"""Raised when a provider call fails in a way the fallback engine should retry on."""
|
|
106
112
|
|
|
@@ -111,6 +117,8 @@ class AdapterError(CodeRouterError):
|
|
|
111
117
|
provider: str,
|
|
112
118
|
status_code: int | None = None,
|
|
113
119
|
retryable: bool = True,
|
|
120
|
+
upstream_body: str | None = None,
|
|
121
|
+
retry_after: str | None = None,
|
|
114
122
|
) -> None:
|
|
115
123
|
"""Construct an AdapterError.
|
|
116
124
|
|
|
@@ -125,11 +133,23 @@ class AdapterError(CodeRouterError):
|
|
|
125
133
|
retryable: When True, the fallback engine may try the next
|
|
126
134
|
provider in the chain. When False, the engine stops
|
|
127
135
|
and surfaces the error as a terminal failure.
|
|
136
|
+
upstream_body: v2.16.0 — the upstream's error body (capped at
|
|
137
|
+
:data:`UPSTREAM_BODY_CAP` chars), kept so the ingress can
|
|
138
|
+
hand the upstream's own wording back to the client.
|
|
139
|
+
Claude Code recovers from several 400s (rejected beta
|
|
140
|
+
headers, tool search, context overflow) only when it can
|
|
141
|
+
read the upstream message. Never logged by this class.
|
|
142
|
+
retry_after: v2.16.0 — the upstream's ``Retry-After`` header
|
|
143
|
+
value, verbatim, when it sent one.
|
|
128
144
|
"""
|
|
129
145
|
super().__init__(message)
|
|
130
146
|
self.provider = provider
|
|
131
147
|
self.status_code = status_code
|
|
132
148
|
self.retryable = retryable
|
|
149
|
+
self.upstream_body = (
|
|
150
|
+
upstream_body[:UPSTREAM_BODY_CAP] if upstream_body is not None else None
|
|
151
|
+
)
|
|
152
|
+
self.retry_after = retry_after
|
|
133
153
|
|
|
134
154
|
def __str__(self) -> str:
|
|
135
155
|
"""Render as ``[provider status=NNN] message`` for log trails."""
|
|
@@ -180,6 +200,23 @@ class StreamTruncatedError(AdapterError):
|
|
|
180
200
|
self.tool_call_in_flight = tool_call_in_flight
|
|
181
201
|
|
|
182
202
|
|
|
203
|
+
# v2.17.0: the provider did not produce its first real streamed output within
|
|
204
|
+
# ``ProviderConfig.ttft_timeout_s``. Raised by the engine (not the adapter),
|
|
205
|
+
# before any byte reached the client, so the chain falls back normally and the
|
|
206
|
+
# self-healing guards learn from it like any other retryable failure.
|
|
207
|
+
class TTFTTimeoutError(AdapterError):
|
|
208
|
+
"""No first token within ``ttft_timeout_s``."""
|
|
209
|
+
|
|
210
|
+
def __init__(self, *, provider: str, ttft_timeout_s: float) -> None:
|
|
211
|
+
super().__init__(
|
|
212
|
+
f"timeout: no first token within {ttft_timeout_s:g}s (ttft_timeout_s)",
|
|
213
|
+
provider=provider,
|
|
214
|
+
status_code=None,
|
|
215
|
+
retryable=True,
|
|
216
|
+
)
|
|
217
|
+
self.ttft_timeout_s = ttft_timeout_s
|
|
218
|
+
|
|
219
|
+
|
|
183
220
|
# v0.6-B: per-call overrides resolved from the active profile. The engine
|
|
184
221
|
# builds one instance per request (since a profile is invariant across its
|
|
185
222
|
# chain) and threads it through every adapter call on that chain. Adapters
|
|
@@ -244,6 +244,8 @@ class OpenAICompatAdapter(BaseAdapter):
|
|
|
244
244
|
provider=self.name,
|
|
245
245
|
status_code=resp.status_code,
|
|
246
246
|
retryable=resp.status_code in _RETRYABLE_STATUSES,
|
|
247
|
+
upstream_body=resp.text,
|
|
248
|
+
retry_after=resp.headers.get("retry-after"),
|
|
247
249
|
)
|
|
248
250
|
|
|
249
251
|
try:
|
|
@@ -381,6 +383,8 @@ class OpenAICompatAdapter(BaseAdapter):
|
|
|
381
383
|
provider=self.name,
|
|
382
384
|
status_code=resp.status_code,
|
|
383
385
|
retryable=resp.status_code in _RETRYABLE_STATUSES,
|
|
386
|
+
upstream_body=body.decode("utf-8", errors="replace"),
|
|
387
|
+
retry_after=resp.headers.get("retry-after"),
|
|
384
388
|
)
|
|
385
389
|
async for line in resp.aiter_lines():
|
|
386
390
|
if not line:
|
|
@@ -165,6 +165,17 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
165
165
|
"for live key values. Exit 0 clean / 2 needs attention / 1 leak."
|
|
166
166
|
),
|
|
167
167
|
)
|
|
168
|
+
doctor.add_argument(
|
|
169
|
+
"--check-claude-code",
|
|
170
|
+
action="store_true",
|
|
171
|
+
help=(
|
|
172
|
+
"v2.16.0: check the local Claude Code install for gateway use — "
|
|
173
|
+
"known-bad versions (<2.1.301, 2.1.304) and the env vars that "
|
|
174
|
+
"matter behind a non-Anthropic gateway (ENABLE_TOOL_SEARCH, "
|
|
175
|
+
"CLAUDE_CODE_MAX_CONTEXT_TOKENS, CLAUDE_CODE_AUTO_MODE_SERVER). "
|
|
176
|
+
"No network. Exit 0 OK / 2 needs attention."
|
|
177
|
+
),
|
|
178
|
+
)
|
|
168
179
|
doctor.add_argument(
|
|
169
180
|
"--config",
|
|
170
181
|
default=None,
|
|
@@ -454,6 +465,28 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
454
465
|
"generation. Tip: keep secrets in a separate .envrc.local."
|
|
455
466
|
),
|
|
456
467
|
)
|
|
468
|
+
vscode_init.add_argument(
|
|
469
|
+
"--max-context-tokens",
|
|
470
|
+
type=int,
|
|
471
|
+
default=None,
|
|
472
|
+
metavar="N",
|
|
473
|
+
help=(
|
|
474
|
+
"v2.16.0: also write CLAUDE_CODE_MAX_CONTEXT_TOKENS=N so Claude "
|
|
475
|
+
"Code compacts before your local model's context fills up "
|
|
476
|
+
"(since 2.1.285 it assumes 1M behind a gateway). Use the "
|
|
477
|
+
"smallest context window in the profile's chain."
|
|
478
|
+
),
|
|
479
|
+
)
|
|
480
|
+
vscode_init.add_argument(
|
|
481
|
+
"--no-claude-code-env",
|
|
482
|
+
action="store_true",
|
|
483
|
+
help=(
|
|
484
|
+
"v2.16.0: do not write the Claude Code gateway settings "
|
|
485
|
+
"(ENABLE_TOOL_SEARCH=false, CLAUDE_CODE_AUTO_MODE_SERVER=0, "
|
|
486
|
+
"and --max-context-tokens). Use when the chain is "
|
|
487
|
+
"api.anthropic.com only."
|
|
488
|
+
),
|
|
489
|
+
)
|
|
457
490
|
vscode_init.add_argument(
|
|
458
491
|
"--dry-run",
|
|
459
492
|
action="store_true",
|
|
@@ -595,10 +628,16 @@ def _run_doctor(args: argparse.Namespace) -> int:
|
|
|
595
628
|
risks AND broken providers can use a single command.
|
|
596
629
|
"""
|
|
597
630
|
check_secrets = bool(getattr(args, "check_secrets", False))
|
|
598
|
-
|
|
631
|
+
check_claude_code = bool(getattr(args, "check_claude_code", False))
|
|
632
|
+
if (
|
|
633
|
+
args.check_model is None
|
|
634
|
+
and args.check_env is None
|
|
635
|
+
and not check_secrets
|
|
636
|
+
and not check_claude_code
|
|
637
|
+
):
|
|
599
638
|
print(
|
|
600
639
|
"doctor: provide --check-model PROVIDER, --check-env [PATH], "
|
|
601
|
-
"and/or --check-
|
|
640
|
+
"--check-secrets and/or --check-claude-code",
|
|
602
641
|
file=sys.stderr,
|
|
603
642
|
)
|
|
604
643
|
return 1
|
|
@@ -614,6 +653,14 @@ def _run_doctor(args: argparse.Namespace) -> int:
|
|
|
614
653
|
if check_secrets:
|
|
615
654
|
worst_exit = max(worst_exit, _run_check_secrets(args.config))
|
|
616
655
|
|
|
656
|
+
if check_claude_code:
|
|
657
|
+
from coderouter.doctor_claude_code import format_report as _cc_format
|
|
658
|
+
from coderouter.doctor_claude_code import run_check as _cc_run
|
|
659
|
+
|
|
660
|
+
cc_report = _cc_run()
|
|
661
|
+
print(_cc_format(cc_report))
|
|
662
|
+
worst_exit = max(worst_exit, cc_report.exit_code)
|
|
663
|
+
|
|
617
664
|
if args.check_model is not None:
|
|
618
665
|
worst_exit = max(worst_exit, _run_check_model(args))
|
|
619
666
|
|
|
@@ -976,6 +1023,7 @@ def _run_vscode_init(args: argparse.Namespace) -> int:
|
|
|
976
1023
|
"""
|
|
977
1024
|
from coderouter.vscode_init import (
|
|
978
1025
|
DEFAULT_PORT,
|
|
1026
|
+
claude_code_env,
|
|
979
1027
|
exit_code_for,
|
|
980
1028
|
format_result,
|
|
981
1029
|
run_vscode_init,
|
|
@@ -991,6 +1039,22 @@ def _run_vscode_init(args: argparse.Namespace) -> int:
|
|
|
991
1039
|
|
|
992
1040
|
port = args.port if args.port is not None else DEFAULT_PORT
|
|
993
1041
|
|
|
1042
|
+
extra_env: dict[str, str] | None = None
|
|
1043
|
+
max_ctx = getattr(args, "max_context_tokens", None)
|
|
1044
|
+
if getattr(args, "no_claude_code_env", False):
|
|
1045
|
+
if max_ctx is not None:
|
|
1046
|
+
print(
|
|
1047
|
+
"vscode-init: --max-context-tokens has no effect with --no-claude-code-env",
|
|
1048
|
+
file=sys.stderr,
|
|
1049
|
+
)
|
|
1050
|
+
return 2
|
|
1051
|
+
else:
|
|
1052
|
+
try:
|
|
1053
|
+
extra_env = claude_code_env(max_context_tokens=max_ctx)
|
|
1054
|
+
except ValueError as exc:
|
|
1055
|
+
print(f"vscode-init: {exc}", file=sys.stderr)
|
|
1056
|
+
return 2
|
|
1057
|
+
|
|
994
1058
|
try:
|
|
995
1059
|
result = run_vscode_init(
|
|
996
1060
|
target,
|
|
@@ -999,6 +1063,7 @@ def _run_vscode_init(args: argparse.Namespace) -> int:
|
|
|
999
1063
|
with_envrc=args.with_envrc,
|
|
1000
1064
|
dry_run=args.dry_run,
|
|
1001
1065
|
force=args.force,
|
|
1066
|
+
extra_env=extra_env,
|
|
1002
1067
|
)
|
|
1003
1068
|
except FileNotFoundError as exc:
|
|
1004
1069
|
# Raised only when target vanishes between the pre-check above
|
|
@@ -550,6 +550,28 @@ class ProviderConfig(BaseModel):
|
|
|
550
550
|
description="If true, only used when ALLOW_PAID=true (plan.md §2.3).",
|
|
551
551
|
)
|
|
552
552
|
timeout_s: float = Field(default=30.0, ge=1.0, le=86400.0)
|
|
553
|
+
# v2.17.0: time-to-first-token cut-off for streaming requests. When the
|
|
554
|
+
# provider has not produced its first real output (text / tool_use /
|
|
555
|
+
# thinking delta) within this many seconds of the attempt starting, the
|
|
556
|
+
# attempt is abandoned and the chain moves on — before any byte reached
|
|
557
|
+
# the client. Never applied to the last provider of a chain (nothing to
|
|
558
|
+
# move on to), nor to non-streaming requests (``timeout_s`` covers
|
|
559
|
+
# those). Before the first *event* it always applies; between the first
|
|
560
|
+
# event and the first real output it needs
|
|
561
|
+
# ``empty_response_action: fallback`` on the profile, which is what
|
|
562
|
+
# withholds those opening events from the client. ``None`` (default)
|
|
563
|
+
# disables it. Typical use: a local llama-server still loading a model
|
|
564
|
+
# or chewing through a long prompt while a cloud fallback could answer.
|
|
565
|
+
ttft_timeout_s: float | None = Field(
|
|
566
|
+
default=None,
|
|
567
|
+
gt=0.0,
|
|
568
|
+
le=3600.0,
|
|
569
|
+
description=(
|
|
570
|
+
"v2.17.0: seconds to wait for the first real streamed output "
|
|
571
|
+
"before falling back to the next provider (not applied to the "
|
|
572
|
+
"last provider in a chain). None disables."
|
|
573
|
+
),
|
|
574
|
+
)
|
|
553
575
|
|
|
554
576
|
# v2.6 language-tax track: path to a LOCAL ``tokenizer.json`` for this
|
|
555
577
|
# provider's model, used to measure the CJK over-count vs the char/4
|
|
@@ -1089,7 +1111,13 @@ class FallbackChain(BaseModel):
|
|
|
1089
1111
|
# (``context_budget_preserve_last_n``) are always
|
|
1090
1112
|
# kept, and tool_use / tool_result pairs are preserved
|
|
1091
1113
|
# atomically to avoid breaking agent loops.
|
|
1092
|
-
|
|
1114
|
+
# * ``reject`` — v2.16.0: ``warn`` + at ``context_budget_trim_threshold``
|
|
1115
|
+
# refuse the request with Anthropic's own overflow error
|
|
1116
|
+
# (HTTP 400, ``prompt is too long: ...``). Claude Code
|
|
1117
|
+
# answers that by compacting the conversation itself
|
|
1118
|
+
# (a summary, not a silent cut), so this is the
|
|
1119
|
+
# recommended action for Claude Code sessions.
|
|
1120
|
+
context_budget_action: Literal["off", "warn", "trim", "reject"] = Field(
|
|
1093
1121
|
default="off",
|
|
1094
1122
|
description=(
|
|
1095
1123
|
"v2.0-F (L1): action when estimated request tokens approach "
|
|
@@ -1097,7 +1125,10 @@ class FallbackChain(BaseModel):
|
|
|
1097
1125
|
"disables the guard entirely. ``warn`` emits a log and "
|
|
1098
1126
|
"response header at ``context_budget_warn_threshold``. "
|
|
1099
1127
|
"``trim`` additionally removes old messages at "
|
|
1100
|
-
"``context_budget_trim_threshold`` to reclaim context space."
|
|
1128
|
+
"``context_budget_trim_threshold`` to reclaim context space. "
|
|
1129
|
+
"``reject`` (v2.16.0) instead answers HTTP 400 "
|
|
1130
|
+
"``prompt is too long`` at that threshold so the client "
|
|
1131
|
+
"compacts the conversation itself (Claude Code does)."
|
|
1101
1132
|
),
|
|
1102
1133
|
)
|
|
1103
1134
|
context_budget_warn_threshold: float = Field(
|
|
@@ -1222,6 +1253,31 @@ class FallbackChain(BaseModel):
|
|
|
1222
1253
|
# --- v2.0-H (L6): Mid-stream partial stitching --------------------------
|
|
1223
1254
|
# * ``off`` — discard partial content on mid-stream failure (legacy).
|
|
1224
1255
|
# * ``surface`` — return partial content as a truncated-but-valid response.
|
|
1256
|
+
# v2.16.0 (tool search): Claude Code 2.1.304+ turns tool search on behind
|
|
1257
|
+
# a custom ANTHROPIC_BASE_URL. The request then carries a server-side
|
|
1258
|
+
# tool-search tool (``type: tool_search_tool_*``) and tools marked
|
|
1259
|
+
# ``defer_loading: true``, which only api.anthropic.com executes. A
|
|
1260
|
+
# local model sees the search tool as an ordinary function (and may
|
|
1261
|
+
# "call" it), while every deferred MCP tool lands in its context anyway.
|
|
1262
|
+
# Claude Code retries without tool search when the gateway answers 400.
|
|
1263
|
+
# * ``auto`` — ``passthrough`` when every provider in the chain
|
|
1264
|
+
# is ``kind: anthropic`` on api.anthropic.com,
|
|
1265
|
+
# otherwise ``reject`` (default).
|
|
1266
|
+
# * ``reject`` — answer 400 so the client turns tool search off.
|
|
1267
|
+
# * ``expand`` — drop the search tool and the ``defer_loading``
|
|
1268
|
+
# flags; every tool is sent up front.
|
|
1269
|
+
# * ``passthrough`` — forward unchanged (pre-v2.16.0 behaviour).
|
|
1270
|
+
deferred_tools_action: Literal["auto", "reject", "expand", "passthrough"] = Field(
|
|
1271
|
+
default="auto",
|
|
1272
|
+
description=(
|
|
1273
|
+
"v2.16.0: what to do with Claude Code tool search "
|
|
1274
|
+
"(``defer_loading`` tools / ``tool_search_tool_*``). ``auto`` "
|
|
1275
|
+
"passes it through only for chains made entirely of "
|
|
1276
|
+
"api.anthropic.com providers and rejects it (HTTP 400, which "
|
|
1277
|
+
"makes Claude Code retry without tool search) otherwise."
|
|
1278
|
+
),
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1225
1281
|
partial_stitch_action: Literal["off", "surface"] = Field(
|
|
1226
1282
|
default="off",
|
|
1227
1283
|
description=(
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""``coderouter doctor --check-claude-code`` (v2.16.0).
|
|
2
|
+
|
|
3
|
+
Local-only checks for a Claude Code install that talks to CodeRouter:
|
|
4
|
+
|
|
5
|
+
1. **Version.** Known gateway-relevant regressions:
|
|
6
|
+
|
|
7
|
+
- before 2.1.301: compaction could repeat when the response's ``model``
|
|
8
|
+
name differs from the requested one (every translating gateway);
|
|
9
|
+
- exactly 2.1.304: every request failed behind a gateway that rejects
|
|
10
|
+
tool search (fixed in 2.1.305).
|
|
11
|
+
|
|
12
|
+
2. **Environment.** Variables that matter behind a non-Anthropic gateway
|
|
13
|
+
(see ``coderouter vscode-init``):
|
|
14
|
+
|
|
15
|
+
- ``ENABLE_TOOL_SEARCH`` unset → tool search is on by default since
|
|
16
|
+
2.1.304; CodeRouter refuses it (``deferred_tools_action``) and Claude
|
|
17
|
+
Code retries without it — works, but costs a round trip per session;
|
|
18
|
+
- ``CLAUDE_CODE_MAX_CONTEXT_TOKENS`` unset on 2.1.285+ → Claude Code may
|
|
19
|
+
assume a 1M window and compact far too late for a local model;
|
|
20
|
+
- ``CLAUDE_CODE_AUTO_MODE_SERVER`` unset → auto mode asks the gateway
|
|
21
|
+
for a server-side classifier a local backend does not have.
|
|
22
|
+
|
|
23
|
+
No network, no config file. Exit codes follow the other doctor checks:
|
|
24
|
+
0 = OK, 2 = needs attention, 1 = could not run.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import shutil
|
|
32
|
+
import subprocess
|
|
33
|
+
from collections.abc import Callable, Mapping
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
|
|
36
|
+
_VERSION_RE = re.compile(r"(\d+)\.(\d+)\.(\d+)")
|
|
37
|
+
|
|
38
|
+
Version = tuple[int, int, int]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ClaudeCodeFinding:
|
|
43
|
+
level: str # "ok" | "warn" | "info"
|
|
44
|
+
message: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ClaudeCodeReport:
|
|
49
|
+
version: Version | None
|
|
50
|
+
findings: list[ClaudeCodeFinding] = field(default_factory=list)
|
|
51
|
+
error: str | None = None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def exit_code(self) -> int:
|
|
55
|
+
if self.error is not None:
|
|
56
|
+
return 1
|
|
57
|
+
return 2 if any(f.level == "warn" for f in self.findings) else 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_version(text: str) -> Version | None:
|
|
61
|
+
m = _VERSION_RE.search(text)
|
|
62
|
+
if m is None:
|
|
63
|
+
return None
|
|
64
|
+
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _fmt(v: Version) -> str:
|
|
68
|
+
return ".".join(str(p) for p in v)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def check(version: Version | None, env: Mapping[str, str]) -> list[ClaudeCodeFinding]:
|
|
72
|
+
"""Pure check: findings for a Claude Code version and an environment."""
|
|
73
|
+
out: list[ClaudeCodeFinding] = []
|
|
74
|
+
if version is not None:
|
|
75
|
+
if version < (2, 1, 301):
|
|
76
|
+
out.append(
|
|
77
|
+
ClaudeCodeFinding(
|
|
78
|
+
"warn",
|
|
79
|
+
f"Claude Code {_fmt(version)} is older than 2.1.301: compaction can "
|
|
80
|
+
"repeat behind a gateway that reports a different model name. "
|
|
81
|
+
"Update Claude Code.",
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
elif version == (2, 1, 304):
|
|
85
|
+
out.append(
|
|
86
|
+
ClaudeCodeFinding(
|
|
87
|
+
"warn",
|
|
88
|
+
"Claude Code 2.1.304 fails every request behind a gateway that "
|
|
89
|
+
"rejects tool search (fixed in 2.1.305). Update Claude Code, or "
|
|
90
|
+
"set ENABLE_TOOL_SEARCH=false.",
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
out.append(ClaudeCodeFinding("ok", f"Claude Code {_fmt(version)}"))
|
|
95
|
+
|
|
96
|
+
if env.get("ENABLE_TOOL_SEARCH", "").lower() in ("false", "0"):
|
|
97
|
+
out.append(ClaudeCodeFinding("ok", "ENABLE_TOOL_SEARCH=false"))
|
|
98
|
+
else:
|
|
99
|
+
out.append(
|
|
100
|
+
ClaudeCodeFinding(
|
|
101
|
+
"warn",
|
|
102
|
+
"ENABLE_TOOL_SEARCH is not false: Claude Code 2.1.304+ starts each "
|
|
103
|
+
"session with tool search, which only api.anthropic.com runs. "
|
|
104
|
+
"CodeRouter refuses it and Claude Code retries without it. Set "
|
|
105
|
+
"ENABLE_TOOL_SEARCH=false (coderouter vscode-init does) to skip "
|
|
106
|
+
"that round trip.",
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if "CLAUDE_CODE_MAX_CONTEXT_TOKENS" in env:
|
|
111
|
+
out.append(
|
|
112
|
+
ClaudeCodeFinding(
|
|
113
|
+
"ok", f"CLAUDE_CODE_MAX_CONTEXT_TOKENS={env['CLAUDE_CODE_MAX_CONTEXT_TOKENS']}"
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
elif version is None or version >= (2, 1, 285):
|
|
117
|
+
out.append(
|
|
118
|
+
ClaudeCodeFinding(
|
|
119
|
+
"warn",
|
|
120
|
+
"CLAUDE_CODE_MAX_CONTEXT_TOKENS is not set: since 2.1.285 Claude Code "
|
|
121
|
+
"may assume a 1M window behind a gateway and compact long after a "
|
|
122
|
+
"local model has overflowed. Set it to your smallest local context "
|
|
123
|
+
"window (coderouter vscode-init --max-context-tokens N), and/or use "
|
|
124
|
+
"context_budget_action: reject in the profile.",
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if env.get("CLAUDE_CODE_AUTO_MODE_SERVER") == "0":
|
|
129
|
+
out.append(ClaudeCodeFinding("ok", "CLAUDE_CODE_AUTO_MODE_SERVER=0"))
|
|
130
|
+
else:
|
|
131
|
+
out.append(
|
|
132
|
+
ClaudeCodeFinding(
|
|
133
|
+
"warn",
|
|
134
|
+
"CLAUDE_CODE_AUTO_MODE_SERVER is not 0: on a gateway, auto mode "
|
|
135
|
+
"defaults to a server-side classifier that a local backend does not "
|
|
136
|
+
"provide. Set CLAUDE_CODE_AUTO_MODE_SERVER=0.",
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
return out
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _claude_version(run: Callable[[list[str]], str] | None = None) -> tuple[Version | None, str | None]:
|
|
143
|
+
if run is None:
|
|
144
|
+
exe = shutil.which("claude")
|
|
145
|
+
if exe is None:
|
|
146
|
+
return None, "`claude` not found on PATH (version checks skipped)"
|
|
147
|
+
|
|
148
|
+
def run(cmd: list[str]) -> str:
|
|
149
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=15, check=False)
|
|
150
|
+
return proc.stdout + proc.stderr
|
|
151
|
+
|
|
152
|
+
cmd = [exe, "--version"]
|
|
153
|
+
else:
|
|
154
|
+
cmd = ["claude", "--version"]
|
|
155
|
+
try:
|
|
156
|
+
text = run(cmd)
|
|
157
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
158
|
+
return None, f"could not run `claude --version`: {exc}"
|
|
159
|
+
version = parse_version(text)
|
|
160
|
+
if version is None:
|
|
161
|
+
return None, f"could not parse `claude --version` output: {text.strip()[:80]!r}"
|
|
162
|
+
return version, None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def run_check(
|
|
166
|
+
env: Mapping[str, str] | None = None,
|
|
167
|
+
run: Callable[[list[str]], str] | None = None,
|
|
168
|
+
) -> ClaudeCodeReport:
|
|
169
|
+
version, note = _claude_version(run)
|
|
170
|
+
findings = check(version, os.environ if env is None else env)
|
|
171
|
+
if note:
|
|
172
|
+
findings.insert(0, ClaudeCodeFinding("info", note))
|
|
173
|
+
return ClaudeCodeReport(version=version, findings=findings)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def format_report(report: ClaudeCodeReport) -> str:
|
|
177
|
+
lines = ["Claude Code (behind CodeRouter)"]
|
|
178
|
+
for f in report.findings:
|
|
179
|
+
mark = {"ok": "OK ", "warn": "WARN", "info": "INFO"}.get(f.level, f.level.upper())
|
|
180
|
+
lines.append(f" [{mark}] {f.message}")
|
|
181
|
+
return "\n".join(lines)
|
|
@@ -87,6 +87,25 @@ if TYPE_CHECKING:
|
|
|
87
87
|
# ---------------------------------------------------------------------------
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
class ContextBudgetRejectError(Exception):
|
|
91
|
+
"""v2.16.0: ``context_budget_action: reject`` refused an oversized request.
|
|
92
|
+
|
|
93
|
+
The ingress answers with Anthropic's overflow error (HTTP 400,
|
|
94
|
+
``prompt is too long: ...``) so the client compacts the conversation
|
|
95
|
+
itself. Carries the numbers for the message and the log line.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, *, estimated_tokens: int, max_context_tokens: int, profile: str) -> None:
|
|
99
|
+
self.estimated_tokens = estimated_tokens
|
|
100
|
+
self.max_context_tokens = max_context_tokens
|
|
101
|
+
self.profile = profile
|
|
102
|
+
super().__init__(
|
|
103
|
+
f"prompt is too long: ~{estimated_tokens} tokens > "
|
|
104
|
+
f"{max_context_tokens} maximum (CodeRouter profile {profile!r}, "
|
|
105
|
+
"estimated)"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
90
109
|
@dataclass(frozen=True, slots=True)
|
|
91
110
|
class ContextBudgetEstimate:
|
|
92
111
|
"""Result of a context-budget estimation check."""
|