coderouter-cli 2.16.0__tar.gz → 2.18.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.16.0 → coderouter_cli-2.18.0}/CHANGELOG.md +84 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/PKG-INFO +1 -1
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/adapters/anthropic_native.py +4 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/adapters/base.py +20 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/adapters/openai_compat.py +6 -0
- coderouter_cli-2.18.0/coderouter/adapters/slot_affinity.py +54 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/cli.py +2 -1
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/config/schemas.py +58 -1
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/doctor_claude_code.py +12 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/anthropic_errors.py +24 -5
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/anthropic_routes.py +20 -6
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/openai_routes.py +7 -2
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/auto_router.py +46 -2
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/capability.py +4 -1
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/fallback.py +94 -6
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/fallback_trace.py +8 -1
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/translation/anthropic.py +3 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/translation/convert.py +4 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/vscode_init.py +10 -1
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/README.en.md +1 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/README.md +1 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/llamacpp-direct.en.md +21 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/llamacpp-direct.md +21 -0
- coderouter_cli-2.18.0/docs/concepts/commit-point.en.md +68 -0
- coderouter_cli-2.18.0/docs/concepts/commit-point.md +70 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/subagent-routing.en.md +19 -2
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/subagent-routing.md +22 -2
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/pyproject.toml +1 -1
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_anthropic_errors.py +30 -1
- coderouter_cli-2.18.0/tests/test_auto_router_hints.py +99 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_context_budget.py +30 -16
- coderouter_cli-2.18.0/tests/test_slot_affinity.py +147 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tool_search.py +1 -0
- coderouter_cli-2.18.0/tests/test_ttft_timeout.py +237 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/.gitignore +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/LICENSE +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/README.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/README.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/config/loader.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/cost.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/credentials.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/errors.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/context_budget.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/self_healing.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/launcher_routes.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/tool_search.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/ui_auth.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/launcher_devices.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/launcher_speculative.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/launcher_swap.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/logging.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/py.typed +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/rollback.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/secret_redaction.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/state/audit_log.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/state/request_log.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/external-agents.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/external-agents.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/gguf_dl.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/hf-ollama-models.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/launcher-quickstart.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/launcher.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/launcher.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/architecture.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/context-budget.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/context-budget.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/continuous-probing.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/drift-detection.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/partial-stitch.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/stream-truncation.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/stream-truncation.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/agent-cli-plugin-extraction.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/external-agents-adapter.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/launcher-model-swap.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/launcher-multi-build.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/orchestration-companion.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/remote-access.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/remote-access.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/security.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/vscode.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/low-memory-integration.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/.env.example +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/README.md +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers-agent-cli.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers-multiagent.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.cli-session.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.llamacpp-vllm.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.opusplan.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.raspberrypi.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.swap.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/__init__.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/conftest.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_agent_cli.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_auto_router.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_budget.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_capability.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_capability_tool_choice.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_cli.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_cli_bind_warning.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_config.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_config_cwd_warning.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_config_shim_actions.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_credentials.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_doctor_claude_code.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_errors.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_examples_yaml_roundtrip.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_cache_strip.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_empty_response.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_reason.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_tool_choice_emulate.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_tool_integrity.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_h1_prometheus_drift.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_h2_h6_translation.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_h3_client_reuse.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_h4_h5_guards.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_h7_probe_restore.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_h8_launcher_auth.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_mA_routing.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_mB_adapters.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_mC_convert.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_mD_logging_metrics.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_mE_config_validation.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_mF_ingress.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_v213_launcher_hardening.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_import_hygiene.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_ingress_count_tokens.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_backend_variants.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_config_bench.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_devices.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_devices_detect.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_devices_routes.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_gui_devices.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_gui_events.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_gui_readiness.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_gui_sweep_close.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_mtp.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_mtp_fallback.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_provider_sync.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_readiness_restart.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_swap.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_swap_review.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_variant_config.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_variant_gui.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_variant_routes.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_variant_sweep.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_models_passthrough.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_packaging.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_plugin_adapter.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_rollback.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_secret_redaction.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_stop_reason_forward_compat.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_stream_truncation.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tool_repair_differential.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tool_repair_lenient.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tool_repair_r4.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tool_repair_scanners.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_toolrepair_bench.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_translation_reverse.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_ui_auth.py +0 -0
- {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_vscode_init.py +0 -0
|
@@ -9,6 +9,90 @@ are kept verbatim where the Japanese text itself is the subject).
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## [v2.18.0] — 2026-10-24 (route on Claude Code's own request hints)
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **auto_router matchers on Claude Code's gateway hint headers.** With
|
|
17
|
+
`CLAUDE_CODE_GATEWAY_HINT_HEADERS=1` (Claude Code 2.1.273+), every request
|
|
18
|
+
says what it is. Three new `RuleMatcher` fields read those headers:
|
|
19
|
+
- `request_class` — `x-claude-code-request-class` (`main` / `subagent` /
|
|
20
|
+
`workflow` / `compaction` / `auxiliary`), case-insensitive;
|
|
21
|
+
- `agent_type` — `x-claude-code-agent-type` (e.g. `Explore`),
|
|
22
|
+
case-insensitive;
|
|
23
|
+
- `compaction: true` — `x-claude-code-compaction` is truthy (`false` is
|
|
24
|
+
rejected at load, like the other boolean matchers).
|
|
25
|
+
Typical use: auto-mode classifier / title calls (`auxiliary`) to the
|
|
26
|
+
smallest local model, compaction to a long-context profile, `Explore`
|
|
27
|
+
sub-agents to a fast one. Without the headers these matchers never match,
|
|
28
|
+
so existing rule sets behave exactly as before. `classify()` gained an
|
|
29
|
+
optional `hints` argument; both `/v1/messages` and `/v1/chat/completions`
|
|
30
|
+
pass the headers through. Docs: `docs/guides/subagent-routing.md` / `.en.md`.
|
|
31
|
+
- **`slot_affinity_slots` (provider; llama.cpp only).** For
|
|
32
|
+
`llama-server --parallel N`: requests carrying a conversation key
|
|
33
|
+
(`x-claude-code-session-id`) get `id_slot = sha256(key) % N`, so every
|
|
34
|
+
turn of a conversation reuses the same slot's prompt cache instead of
|
|
35
|
+
re-prefilling on whichever slot is free. Stable across restarts; an
|
|
36
|
+
explicit `id_slot` in `extra_body` wins; off by default. Works on both the
|
|
37
|
+
native (`kind: anthropic`) and translated (`kind: openai_compat`) paths,
|
|
38
|
+
and survives every internal request rebuild (thinking strip for backends
|
|
39
|
+
without thinking, `tool_choice` emulation, `cache_control` strip,
|
|
40
|
+
OpenAI→Anthropic translation). The main conversation and its compaction
|
|
41
|
+
share a key; sub-agents and auxiliary calls get `<session>:<class>:<agent>`
|
|
42
|
+
so they do not queue on — or evict — the main conversation's slot.
|
|
43
|
+
Docs: `docs/backends/llamacpp-direct.md` / `.en.md`.
|
|
44
|
+
- **`vscode-init` also writes `CLAUDE_CODE_GATEWAY_HINT_HEADERS=1`**, and
|
|
45
|
+
`doctor --check-claude-code` reports (as info) when it is missing.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## [v2.17.0] — 2026-10-24 (time-to-first-token cut-off, retry signals)
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **`ttft_timeout_s` (provider).** A streaming attempt that has not
|
|
54
|
+
produced its first real output (text / tool_use / thinking delta) within
|
|
55
|
+
this many seconds is abandoned before any byte reached the client, and
|
|
56
|
+
the chain moves on — e.g. a local llama-server still loading a model or
|
|
57
|
+
prefilling a long prompt while a cloud provider could answer now. Never
|
|
58
|
+
applied to the last provider of a chain (nothing to fall back to), nor to
|
|
59
|
+
non-streaming requests (`timeout_s` covers those), nor to the
|
|
60
|
+
`/v1/chat/completions` route. It is also skipped for the tool-turn
|
|
61
|
+
downgrade (`kind: openai_compat` with tools, which calls the upstream
|
|
62
|
+
non-streaming so tool calls can be repaired): there the "first output" is
|
|
63
|
+
the whole answer, and a deadline would discard slow but healthy replies —
|
|
64
|
+
use `kind: anthropic` (native `/v1/messages`) to get the cut-off for a
|
|
65
|
+
local server. It always covers the
|
|
66
|
+
wait for the first event; covering the gap between the opening events and
|
|
67
|
+
the first real output needs `empty_response_action: fallback`, which is
|
|
68
|
+
what withholds those events from the client. Abandoned attempts are
|
|
69
|
+
ordinary retryable failures: new fallback reason `ttft-timeout` (detail
|
|
70
|
+
`no-first-token`) and the self-healing guards learn from them. A cut that
|
|
71
|
+
happens after the opening events is handled like a truncated stream: the
|
|
72
|
+
withheld, unterminated preamble is never flushed to the client if the
|
|
73
|
+
rest of the chain also fails, and the abandoned upstream stream is closed
|
|
74
|
+
immediately. Default
|
|
75
|
+
`None` (off). Docs: `docs/concepts/commit-point.md` / `.en.md`.
|
|
76
|
+
- **`x-should-retry` on `/v1/messages` errors.** Every Anthropic-shaped
|
|
77
|
+
error CodeRouter produces now says whether retrying can help — the header
|
|
78
|
+
Claude Code reads, with `retry-after`, to decide whether to spend its
|
|
79
|
+
retry budget: `false` for 400 / 413 (including tool-search and
|
|
80
|
+
`context_budget_action: reject` refusals), `true` for 429, and for 502
|
|
81
|
+
`true` only when at least one upstream failure was retryable (a chain that
|
|
82
|
+
failed purely on 401/403 fails the same way on every retry).
|
|
83
|
+
- **Docs: the commit point** (`docs/concepts/commit-point.md` / `.en.md`) —
|
|
84
|
+
which failures before the body ships CodeRouter can still move to another
|
|
85
|
+
provider, which it cannot, and the error each chain failure becomes.
|
|
86
|
+
Recommends `empty_response_action: fallback` for Claude Code profiles.
|
|
87
|
+
|
|
88
|
+
### Known limits
|
|
89
|
+
|
|
90
|
+
- No keep-alive `ping` is sent while opening events are withheld; keep
|
|
91
|
+
`ttft_timeout_s` below the client's idle timeout
|
|
92
|
+
(`CLAUDE_STREAM_IDLE_TIMEOUT_MS` for Claude Code).
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
12
96
|
## [v2.16.0] — 2026-10-24 (Claude Code 2.1.3xx behind a gateway)
|
|
13
97
|
|
|
14
98
|
Claude Code 2.1.285–2.1.309 changed several defaults for sessions behind a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.18.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
|
|
@@ -46,6 +46,7 @@ from coderouter.adapters.base import (
|
|
|
46
46
|
StreamChunk,
|
|
47
47
|
StreamTruncatedError,
|
|
48
48
|
)
|
|
49
|
+
from coderouter.adapters.slot_affinity import apply_slot_affinity
|
|
49
50
|
from coderouter.credentials import resolve_provider_credential
|
|
50
51
|
from coderouter.logging import (
|
|
51
52
|
get_logger,
|
|
@@ -243,6 +244,9 @@ class AnthropicAdapter(BaseAdapter):
|
|
|
243
244
|
body.update(dumped)
|
|
244
245
|
body["model"] = self.config.model
|
|
245
246
|
body["stream"] = stream
|
|
247
|
+
apply_slot_affinity(
|
|
248
|
+
body, slots=self.config.slot_affinity_slots, key=req.session_key
|
|
249
|
+
)
|
|
246
250
|
return body
|
|
247
251
|
|
|
248
252
|
# ------------------------------------------------------------------
|
|
@@ -65,6 +65,9 @@ class ChatRequest(BaseModel):
|
|
|
65
65
|
|
|
66
66
|
# CodeRouter-specific extension (not sent upstream)
|
|
67
67
|
profile: str | None = Field(default=None, exclude=True)
|
|
68
|
+
# v2.18.0: conversation key for KV-slot affinity (see
|
|
69
|
+
# ``ProviderConfig.slot_affinity_slots``). Never serialized.
|
|
70
|
+
session_key: str | None = Field(default=None, exclude=True)
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
class ChatResponse(BaseModel):
|
|
@@ -200,6 +203,23 @@ class StreamTruncatedError(AdapterError):
|
|
|
200
203
|
self.tool_call_in_flight = tool_call_in_flight
|
|
201
204
|
|
|
202
205
|
|
|
206
|
+
# v2.17.0: the provider did not produce its first real streamed output within
|
|
207
|
+
# ``ProviderConfig.ttft_timeout_s``. Raised by the engine (not the adapter),
|
|
208
|
+
# before any byte reached the client, so the chain falls back normally and the
|
|
209
|
+
# self-healing guards learn from it like any other retryable failure.
|
|
210
|
+
class TTFTTimeoutError(AdapterError):
|
|
211
|
+
"""No first token within ``ttft_timeout_s``."""
|
|
212
|
+
|
|
213
|
+
def __init__(self, *, provider: str, ttft_timeout_s: float) -> None:
|
|
214
|
+
super().__init__(
|
|
215
|
+
f"timeout: no first token within {ttft_timeout_s:g}s (ttft_timeout_s)",
|
|
216
|
+
provider=provider,
|
|
217
|
+
status_code=None,
|
|
218
|
+
retryable=True,
|
|
219
|
+
)
|
|
220
|
+
self.ttft_timeout_s = ttft_timeout_s
|
|
221
|
+
|
|
222
|
+
|
|
203
223
|
# v0.6-B: per-call overrides resolved from the active profile. The engine
|
|
204
224
|
# builds one instance per request (since a profile is invariant across its
|
|
205
225
|
# chain) and threads it through every adapter call on that chain. Adapters
|
|
@@ -30,6 +30,7 @@ from coderouter.adapters.base import (
|
|
|
30
30
|
StreamChunk,
|
|
31
31
|
StreamTruncatedError,
|
|
32
32
|
)
|
|
33
|
+
from coderouter.adapters.slot_affinity import apply_slot_affinity
|
|
33
34
|
from coderouter.credentials import resolve_provider_credential
|
|
34
35
|
from coderouter.logging import (
|
|
35
36
|
get_logger,
|
|
@@ -182,6 +183,11 @@ class OpenAICompatAdapter(BaseAdapter):
|
|
|
182
183
|
value = getattr(request, field, None)
|
|
183
184
|
if value is not None:
|
|
184
185
|
body[field] = value
|
|
186
|
+
apply_slot_affinity(
|
|
187
|
+
body,
|
|
188
|
+
slots=self.config.slot_affinity_slots,
|
|
189
|
+
key=getattr(request, "session_key", None),
|
|
190
|
+
)
|
|
185
191
|
if stream:
|
|
186
192
|
# Request a terminal usage chunk. Providers that honor this
|
|
187
193
|
# (OpenAI, OpenRouter, Ollama >=0.x) will send one extra chunk
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""v2.18.0: KV-slot affinity for llama-server (``id_slot``).
|
|
2
|
+
|
|
3
|
+
llama-server started with ``--parallel N`` serves N slots, each holding its
|
|
4
|
+
own prompt cache. Without a hint it picks a free slot, so consecutive turns
|
|
5
|
+
of one conversation can land on different slots and re-prefill the whole
|
|
6
|
+
prompt. Sending ``id_slot`` pins a conversation to one slot.
|
|
7
|
+
|
|
8
|
+
The slot is ``sha256(key) % N`` — stable across processes and restarts
|
|
9
|
+
(Python's ``hash`` is salted per process).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
# Request classes whose prompt shares the main conversation's prefix: they
|
|
19
|
+
# belong on the main conversation's slot. Everything else (sub-agents,
|
|
20
|
+
# auxiliary calls such as the auto-mode classifier or titles, workflows) has
|
|
21
|
+
# its own prompt and gets its own key, so it neither waits on the main slot
|
|
22
|
+
# nor evicts its cache.
|
|
23
|
+
_MAIN_CLASSES = frozenset({"", "main", "compaction"})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def affinity_key(hints: Mapping[str, str]) -> str | None:
|
|
27
|
+
"""Conversation key for slot affinity, from Claude Code's hint headers.
|
|
28
|
+
|
|
29
|
+
``None`` when there is no session id. ``main`` / ``compaction`` (and a
|
|
30
|
+
missing class) share ``<session>``; other classes get
|
|
31
|
+
``<session>:<class>:<agent_type>``.
|
|
32
|
+
"""
|
|
33
|
+
session = hints.get("session_id")
|
|
34
|
+
if not session:
|
|
35
|
+
return None
|
|
36
|
+
request_class = hints.get("request_class", "").lower()
|
|
37
|
+
if request_class in _MAIN_CLASSES:
|
|
38
|
+
return session
|
|
39
|
+
return f"{session}:{request_class}:{hints.get('agent_type', '').lower()}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def slot_for(key: str, slots: int) -> int:
|
|
43
|
+
digest = hashlib.sha256(key.encode("utf-8")).digest()
|
|
44
|
+
return int.from_bytes(digest[:8], "big") % slots
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def apply_slot_affinity(body: dict[str, Any], *, slots: int | None, key: str | None) -> None:
|
|
48
|
+
"""Set ``body["id_slot"]`` when affinity is configured and a key exists.
|
|
49
|
+
|
|
50
|
+
An ``id_slot`` already present (e.g. from ``extra_body``) wins.
|
|
51
|
+
"""
|
|
52
|
+
if not slots or not key or "id_slot" in body:
|
|
53
|
+
return
|
|
54
|
+
body["id_slot"] = slot_for(key, slots)
|
|
@@ -483,7 +483,8 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
483
483
|
help=(
|
|
484
484
|
"v2.16.0: do not write the Claude Code gateway settings "
|
|
485
485
|
"(ENABLE_TOOL_SEARCH=false, CLAUDE_CODE_AUTO_MODE_SERVER=0, "
|
|
486
|
-
"and --max-context-tokens).
|
|
486
|
+
"CLAUDE_CODE_GATEWAY_HINT_HEADERS=1 and --max-context-tokens). "
|
|
487
|
+
"Use when the chain is "
|
|
487
488
|
"api.anthropic.com only."
|
|
488
489
|
),
|
|
489
490
|
)
|
|
@@ -550,6 +550,45 @@ 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
|
+
# v2.18.0: KV-slot affinity for llama-server started with
|
|
566
|
+
# ``--parallel N``. When set to N and the request carries a conversation
|
|
567
|
+
# key (Claude Code's ``x-claude-code-session-id`` hint header), the body
|
|
568
|
+
# gets ``id_slot = stable_hash(key) % N`` so every turn of a conversation
|
|
569
|
+
# lands on the same slot and reuses its prompt cache instead of
|
|
570
|
+
# re-prefilling on whichever slot is free. llama.cpp only (other servers
|
|
571
|
+
# ignore or reject ``id_slot``); ``None`` (default) sends nothing.
|
|
572
|
+
slot_affinity_slots: int | None = Field(
|
|
573
|
+
default=None,
|
|
574
|
+
ge=1,
|
|
575
|
+
le=256,
|
|
576
|
+
description=(
|
|
577
|
+
"v2.18.0: llama-server --parallel N. Pins each conversation to "
|
|
578
|
+
"slot hash(session) % N via id_slot so its KV cache is reused. "
|
|
579
|
+
"llama.cpp only. None disables."
|
|
580
|
+
),
|
|
581
|
+
)
|
|
582
|
+
ttft_timeout_s: float | None = Field(
|
|
583
|
+
default=None,
|
|
584
|
+
gt=0.0,
|
|
585
|
+
le=3600.0,
|
|
586
|
+
description=(
|
|
587
|
+
"v2.17.0: seconds to wait for the first real streamed output "
|
|
588
|
+
"before falling back to the next provider (not applied to the "
|
|
589
|
+
"last provider in a chain). None disables."
|
|
590
|
+
),
|
|
591
|
+
)
|
|
553
592
|
|
|
554
593
|
# v2.6 language-tax track: path to a LOCAL ``tokenizer.json`` for this
|
|
555
594
|
# provider's model, used to measure the CJK over-count vs the char/4
|
|
@@ -1617,6 +1656,21 @@ class RuleMatcher(BaseModel):
|
|
|
1617
1656
|
# declares any tools); ``False`` is rejected by ``_exactly_one``
|
|
1618
1657
|
# since a "no-tools" rule would shadow the default fall-through.
|
|
1619
1658
|
has_tools: bool | None = None
|
|
1659
|
+
# v2.18.0: Claude Code gateway hint headers. Claude Code 2.1.273+ sends
|
|
1660
|
+
# these to a custom ANTHROPIC_BASE_URL when
|
|
1661
|
+
# ``CLAUDE_CODE_GATEWAY_HINT_HEADERS=1`` is set (``coderouter
|
|
1662
|
+
# vscode-init`` sets it). They say what a request *is* — something the
|
|
1663
|
+
# body heuristics above can only guess:
|
|
1664
|
+
# * ``request_class`` — ``x-claude-code-request-class``: ``main`` /
|
|
1665
|
+
# ``subagent`` / ``workflow`` / ``compaction`` / ``auxiliary``
|
|
1666
|
+
# (auto-mode classifier, titles, …). Case-insensitive exact match.
|
|
1667
|
+
# * ``agent_type`` — ``x-claude-code-agent-type`` (e.g. ``Explore``),
|
|
1668
|
+
# case-insensitive exact match.
|
|
1669
|
+
# * ``compaction: True`` — ``x-claude-code-compaction`` is truthy.
|
|
1670
|
+
# Without the headers these matchers simply never match.
|
|
1671
|
+
request_class: str | None = None
|
|
1672
|
+
agent_type: str | None = None
|
|
1673
|
+
compaction: bool | None = None
|
|
1620
1674
|
|
|
1621
1675
|
_MATCHER_FIELDS: tuple[str, ...] = (
|
|
1622
1676
|
"has_image",
|
|
@@ -1627,12 +1681,15 @@ class RuleMatcher(BaseModel):
|
|
|
1627
1681
|
"content_token_count_min",
|
|
1628
1682
|
"has_tools",
|
|
1629
1683
|
"cjk_ratio_min",
|
|
1684
|
+
"request_class",
|
|
1685
|
+
"agent_type",
|
|
1686
|
+
"compaction",
|
|
1630
1687
|
)
|
|
1631
1688
|
|
|
1632
1689
|
# Boolean matcher fields carry meaning only at ``True`` — the runtime
|
|
1633
1690
|
# evaluator uses ``is True`` — so a ``False`` value is a dead rule that
|
|
1634
1691
|
# never matches. ``_exactly_one`` rejects it explicitly (see below).
|
|
1635
|
-
_BOOL_MATCHER_FIELDS: tuple[str, ...] = ("has_image", "has_tools")
|
|
1692
|
+
_BOOL_MATCHER_FIELDS: tuple[str, ...] = ("has_image", "has_tools", "compaction")
|
|
1636
1693
|
|
|
1637
1694
|
@model_validator(mode="after")
|
|
1638
1695
|
def _exactly_one(self) -> Self:
|
|
@@ -125,6 +125,18 @@ def check(version: Version | None, env: Mapping[str, str]) -> list[ClaudeCodeFin
|
|
|
125
125
|
)
|
|
126
126
|
)
|
|
127
127
|
|
|
128
|
+
if env.get("CLAUDE_CODE_GATEWAY_HINT_HEADERS", "").lower() in ("1", "true"):
|
|
129
|
+
out.append(ClaudeCodeFinding("ok", "CLAUDE_CODE_GATEWAY_HINT_HEADERS=1"))
|
|
130
|
+
else:
|
|
131
|
+
out.append(
|
|
132
|
+
ClaudeCodeFinding(
|
|
133
|
+
"info",
|
|
134
|
+
"CLAUDE_CODE_GATEWAY_HINT_HEADERS is not set: auto_router rules "
|
|
135
|
+
"on request_class / agent_type / compaction (v2.18.0) never "
|
|
136
|
+
"match without it.",
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
128
140
|
if env.get("CLAUDE_CODE_AUTO_MODE_SERVER") == "0":
|
|
129
141
|
out.append(ClaudeCodeFinding("ok", "CLAUDE_CODE_AUTO_MODE_SERVER=0"))
|
|
130
142
|
else:
|
|
@@ -33,6 +33,13 @@ terminal error 429 429 ``rate_limit_error``
|
|
|
33
33
|
anything else 502 ``api_error``
|
|
34
34
|
==================================== ====== ========================
|
|
35
35
|
|
|
36
|
+
v2.17.0: every mapped response also carries ``x-should-retry`` — the header
|
|
37
|
+
Claude Code reads (with ``retry-after``) to decide whether to spend its retry
|
|
38
|
+
budget. ``false`` for the 4xx rows (resending the same request cannot
|
|
39
|
+
help), ``true`` for 429, and for 502 ``true`` only when at least one
|
|
40
|
+
upstream failure was retryable — a chain that failed purely on, say, 401s
|
|
41
|
+
will fail the same way on every retry.
|
|
42
|
+
|
|
36
43
|
Upstream 401 / 403 deliberately fall into the last row: they describe
|
|
37
44
|
CodeRouter's *own* upstream credentials, not the client's, and handing a
|
|
38
45
|
401 to Claude Code would send the user to ``/login`` for nothing.
|
|
@@ -164,17 +171,26 @@ def map_no_providers_error(exc: NoProvidersAvailableError) -> AnthropicErrorResp
|
|
|
164
171
|
msg = upstream_message(err)
|
|
165
172
|
if not msg.lower().startswith(PROMPT_TOO_LONG_PREFIX):
|
|
166
173
|
msg = f"{PROMPT_TOO_LONG_PREFIX}: {msg}"
|
|
167
|
-
return AnthropicErrorResponse(
|
|
174
|
+
return AnthropicErrorResponse(
|
|
175
|
+
400, "invalid_request_error", msg, {"x-should-retry": "false"}
|
|
176
|
+
)
|
|
168
177
|
|
|
169
178
|
terminal = errors[-1] if errors else None
|
|
170
179
|
if terminal is not None and terminal.status_code is not None:
|
|
171
180
|
status = terminal.status_code
|
|
172
181
|
if status in (400, 422) and not terminal.retryable:
|
|
173
|
-
return AnthropicErrorResponse(
|
|
182
|
+
return AnthropicErrorResponse(
|
|
183
|
+
400,
|
|
184
|
+
"invalid_request_error",
|
|
185
|
+
upstream_message(terminal),
|
|
186
|
+
{"x-should-retry": "false"},
|
|
187
|
+
)
|
|
174
188
|
if status == 413:
|
|
175
|
-
return AnthropicErrorResponse(
|
|
189
|
+
return AnthropicErrorResponse(
|
|
190
|
+
413, "request_too_large", upstream_message(terminal), {"x-should-retry": "false"}
|
|
191
|
+
)
|
|
176
192
|
if status == 429:
|
|
177
|
-
headers: dict[str, str] = {}
|
|
193
|
+
headers: dict[str, str] = {"x-should-retry": "true"}
|
|
178
194
|
retry_after = getattr(terminal, "retry_after", None)
|
|
179
195
|
if retry_after:
|
|
180
196
|
headers["retry-after"] = retry_after
|
|
@@ -182,4 +198,7 @@ def map_no_providers_error(exc: NoProvidersAvailableError) -> AnthropicErrorResp
|
|
|
182
198
|
429, "rate_limit_error", upstream_message(terminal), headers
|
|
183
199
|
)
|
|
184
200
|
|
|
185
|
-
|
|
201
|
+
retry = any(e.retryable for e in errors)
|
|
202
|
+
return AnthropicErrorResponse(
|
|
203
|
+
502, "api_error", redact(str(exc)), {"x-should-retry": "true" if retry else "false"}
|
|
204
|
+
)
|
|
@@ -31,6 +31,7 @@ from fastapi import APIRouter, Header, HTTPException, Request
|
|
|
31
31
|
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
|
32
32
|
|
|
33
33
|
from coderouter.adapters.base import StreamTruncatedError
|
|
34
|
+
from coderouter.adapters.slot_affinity import affinity_key
|
|
34
35
|
from coderouter.guards.context_budget import ContextBudgetRejectError
|
|
35
36
|
from coderouter.guards.tool_loop import ToolCountExceededError, ToolLoopBreakError
|
|
36
37
|
from coderouter.ingress.anthropic_errors import (
|
|
@@ -49,7 +50,7 @@ from coderouter.routing import (
|
|
|
49
50
|
MidStreamError,
|
|
50
51
|
NoProvidersAvailableError,
|
|
51
52
|
)
|
|
52
|
-
from coderouter.routing.auto_router import RESERVED_PROFILE_NAME, classify
|
|
53
|
+
from coderouter.routing.auto_router import RESERVED_PROFILE_NAME, classify, extract_gateway_hints
|
|
53
54
|
from coderouter.routing.fallback_trace import (
|
|
54
55
|
SSE_FALLBACK_EVENT,
|
|
55
56
|
current_fallback_trace,
|
|
@@ -226,6 +227,11 @@ async def messages(
|
|
|
226
227
|
if anthropic_beta:
|
|
227
228
|
anth_req.anthropic_beta = anthropic_beta
|
|
228
229
|
|
|
230
|
+
# v2.18.0: Claude Code gateway hint headers — routing matchers below,
|
|
231
|
+
# and the conversation key for llama-server KV-slot affinity.
|
|
232
|
+
gateway_hints = extract_gateway_hints(request.headers)
|
|
233
|
+
anth_req.session_key = affinity_key(gateway_hints)
|
|
234
|
+
|
|
229
235
|
# Profile selection — body field wins over header (same policy as OpenAI route).
|
|
230
236
|
if anth_req.profile is None and x_coderouter_profile:
|
|
231
237
|
anth_req.profile = x_coderouter_profile
|
|
@@ -253,7 +259,7 @@ async def messages(
|
|
|
253
259
|
# AnthropicRequest), so both OpenAI and Anthropic ingress use the same
|
|
254
260
|
# classifier without a shared request shim.
|
|
255
261
|
if anth_req.profile is None and config.default_profile == RESERVED_PROFILE_NAME:
|
|
256
|
-
anth_req.profile = classify(payload, config)
|
|
262
|
+
anth_req.profile = classify(payload, config, gateway_hints)
|
|
257
263
|
|
|
258
264
|
if anth_req.profile is not None:
|
|
259
265
|
try:
|
|
@@ -276,7 +282,9 @@ async def messages(
|
|
|
276
282
|
)
|
|
277
283
|
if ts_action == "reject":
|
|
278
284
|
return _anthropic_error_response(
|
|
279
|
-
AnthropicErrorResponse(
|
|
285
|
+
AnthropicErrorResponse(
|
|
286
|
+
400, "invalid_request_error", REJECT_MESSAGE, {"x-should-retry": "false"}
|
|
287
|
+
)
|
|
280
288
|
)
|
|
281
289
|
if ts_action == "expand":
|
|
282
290
|
anth_req = expand_tool_search(anth_req)
|
|
@@ -290,7 +298,9 @@ async def messages(
|
|
|
290
298
|
# v2.16.0: ``context_budget_action: reject`` — Anthropic's own
|
|
291
299
|
# overflow answer, so Claude Code compacts the conversation.
|
|
292
300
|
return _anthropic_error_response(
|
|
293
|
-
AnthropicErrorResponse(
|
|
301
|
+
AnthropicErrorResponse(
|
|
302
|
+
400, "invalid_request_error", str(exc), {"x-should-retry": "false"}
|
|
303
|
+
)
|
|
294
304
|
)
|
|
295
305
|
|
|
296
306
|
if anth_req.stream:
|
|
@@ -362,7 +372,9 @@ async def messages(
|
|
|
362
372
|
# swap) — same answer as the pre-dispatch check above.
|
|
363
373
|
await _aclose_quietly(source)
|
|
364
374
|
return _anthropic_error_response(
|
|
365
|
-
AnthropicErrorResponse(
|
|
375
|
+
AnthropicErrorResponse(
|
|
376
|
+
400, "invalid_request_error", str(exc), {"x-should-retry": "false"}
|
|
377
|
+
)
|
|
366
378
|
)
|
|
367
379
|
except (ToolLoopBreakError, ToolCountExceededError, MidStreamError) as exc:
|
|
368
380
|
# Policy errors and mid-stream failures keep their existing
|
|
@@ -415,7 +427,9 @@ async def messages(
|
|
|
415
427
|
except ContextBudgetRejectError as exc:
|
|
416
428
|
# See the streaming branch: the engine can re-run the guard.
|
|
417
429
|
return _anthropic_error_response(
|
|
418
|
-
AnthropicErrorResponse(
|
|
430
|
+
AnthropicErrorResponse(
|
|
431
|
+
400, "invalid_request_error", str(exc), {"x-should-retry": "false"}
|
|
432
|
+
)
|
|
419
433
|
)
|
|
420
434
|
except NoProvidersAvailableError as exc:
|
|
421
435
|
# v2.16.0: Anthropic-shaped error with the upstream's own wording
|
|
@@ -32,9 +32,10 @@ from fastapi import APIRouter, Header, HTTPException, Request
|
|
|
32
32
|
from fastapi.responses import JSONResponse, StreamingResponse
|
|
33
33
|
|
|
34
34
|
from coderouter.adapters.base import ChatRequest
|
|
35
|
+
from coderouter.adapters.slot_affinity import affinity_key
|
|
35
36
|
from coderouter.logging import get_logger
|
|
36
37
|
from coderouter.routing import FallbackEngine, NoProvidersAvailableError
|
|
37
|
-
from coderouter.routing.auto_router import RESERVED_PROFILE_NAME, classify
|
|
38
|
+
from coderouter.routing.auto_router import RESERVED_PROFILE_NAME, classify, extract_gateway_hints
|
|
38
39
|
from coderouter.routing.fallback_trace import current_fallback_trace
|
|
39
40
|
|
|
40
41
|
router = APIRouter()
|
|
@@ -199,6 +200,10 @@ async def chat_completions(
|
|
|
199
200
|
except Exception as exc: # pydantic.ValidationError, etc.
|
|
200
201
|
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
201
202
|
|
|
203
|
+
# v2.18.0: gateway hint headers (routing matchers + KV-slot affinity key).
|
|
204
|
+
gateway_hints = extract_gateway_hints(request.headers)
|
|
205
|
+
chat_req.session_key = affinity_key(gateway_hints)
|
|
206
|
+
|
|
202
207
|
# Header-based override (body wins if both are set — see module docstring)
|
|
203
208
|
if chat_req.profile is None and x_coderouter_profile:
|
|
204
209
|
chat_req.profile = x_coderouter_profile
|
|
@@ -226,7 +231,7 @@ async def chat_completions(
|
|
|
226
231
|
# When inactive, the engine still falls through to
|
|
227
232
|
# ``config.default_profile`` on its own — same semantics as pre-v1.6.
|
|
228
233
|
if chat_req.profile is None and config.default_profile == RESERVED_PROFILE_NAME:
|
|
229
|
-
chat_req.profile = classify(payload, config)
|
|
234
|
+
chat_req.profile = classify(payload, config, gateway_hints)
|
|
230
235
|
|
|
231
236
|
# Validate profile exists before we kick off any upstream call
|
|
232
237
|
if chat_req.profile is not None:
|
|
@@ -36,6 +36,7 @@ from __future__ import annotations
|
|
|
36
36
|
|
|
37
37
|
import logging
|
|
38
38
|
import re
|
|
39
|
+
from collections.abc import Mapping
|
|
39
40
|
from typing import TYPE_CHECKING, Any
|
|
40
41
|
|
|
41
42
|
from coderouter.config.schemas import AutoRouterConfig, AutoRouteRule, RuleMatcher
|
|
@@ -169,6 +170,32 @@ def _has_tools_in_body(body: dict[str, Any]) -> bool:
|
|
|
169
170
|
return isinstance(functions, list) and len(functions) > 0
|
|
170
171
|
|
|
171
172
|
|
|
173
|
+
# v2.18.0: Claude Code gateway hint headers → hint keys.
|
|
174
|
+
_HINT_HEADERS: dict[str, str] = {
|
|
175
|
+
"x-claude-code-request-class": "request_class",
|
|
176
|
+
"x-claude-code-agent-type": "agent_type",
|
|
177
|
+
"x-claude-code-compaction": "compaction",
|
|
178
|
+
"x-claude-code-session-id": "session_id",
|
|
179
|
+
"x-claude-code-prompt-id": "prompt_id",
|
|
180
|
+
}
|
|
181
|
+
_TRUTHY = frozenset({"1", "true", "yes", "on"})
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def extract_gateway_hints(headers: Mapping[str, str]) -> dict[str, str]:
|
|
185
|
+
"""Pull Claude Code's gateway hint headers out of a request.
|
|
186
|
+
|
|
187
|
+
Returns only the hints that are present (lower-cased keys from
|
|
188
|
+
:data:`_HINT_HEADERS`). Header lookup is case-insensitive whenever the
|
|
189
|
+
mapping is (Starlette's ``Headers`` is).
|
|
190
|
+
"""
|
|
191
|
+
out: dict[str, str] = {}
|
|
192
|
+
for header, key in _HINT_HEADERS.items():
|
|
193
|
+
value = headers.get(header)
|
|
194
|
+
if value:
|
|
195
|
+
out[key] = value.strip()
|
|
196
|
+
return out
|
|
197
|
+
|
|
198
|
+
|
|
172
199
|
def _match_rule(
|
|
173
200
|
rule: AutoRouteRule,
|
|
174
201
|
message: dict[str, Any] | None,
|
|
@@ -176,8 +203,17 @@ def _match_rule(
|
|
|
176
203
|
model: str | None,
|
|
177
204
|
estimated_tokens: int,
|
|
178
205
|
has_tools: bool,
|
|
206
|
+
hints: Mapping[str, str] | None = None,
|
|
179
207
|
) -> bool:
|
|
180
208
|
m = rule.match
|
|
209
|
+
if m.request_class is not None:
|
|
210
|
+
value = (hints or {}).get("request_class")
|
|
211
|
+
return value is not None and value.lower() == m.request_class.lower()
|
|
212
|
+
if m.agent_type is not None:
|
|
213
|
+
value = (hints or {}).get("agent_type")
|
|
214
|
+
return value is not None and value.lower() == m.agent_type.lower()
|
|
215
|
+
if m.compaction is True:
|
|
216
|
+
return (hints or {}).get("compaction", "").lower() in _TRUTHY
|
|
181
217
|
if m.has_image is True:
|
|
182
218
|
return message is not None and _has_image(message)
|
|
183
219
|
if m.code_fence_ratio_min is not None:
|
|
@@ -237,7 +273,11 @@ def _extract_model(body: dict[str, Any]) -> str | None:
|
|
|
237
273
|
return None
|
|
238
274
|
|
|
239
275
|
|
|
240
|
-
def classify(
|
|
276
|
+
def classify(
|
|
277
|
+
body: dict[str, Any],
|
|
278
|
+
config: CodeRouterConfig,
|
|
279
|
+
hints: Mapping[str, str] | None = None,
|
|
280
|
+
) -> str:
|
|
241
281
|
"""Resolve an incoming request body to a profile name.
|
|
242
282
|
|
|
243
283
|
Rule order (first match wins) is determined by:
|
|
@@ -252,6 +292,10 @@ def classify(body: dict[str, Any], config: CodeRouterConfig) -> str:
|
|
|
252
292
|
|
|
253
293
|
Emits one of two log events: ``auto-router-resolved`` on match, or
|
|
254
294
|
``auto-router-fallthrough`` on default-rule fall.
|
|
295
|
+
|
|
296
|
+
v2.18.0: ``hints`` carries Claude Code's gateway hint headers (see
|
|
297
|
+
:func:`extract_gateway_hints`) for the ``request_class`` /
|
|
298
|
+
``agent_type`` / ``compaction`` matchers.
|
|
255
299
|
"""
|
|
256
300
|
user_msg = _latest_user_message(body)
|
|
257
301
|
text = _extract_text(user_msg) if user_msg is not None else ""
|
|
@@ -294,7 +338,7 @@ def classify(body: dict[str, Any], config: CodeRouterConfig) -> str:
|
|
|
294
338
|
# present — they short out via ``_match_rule``'s message-None
|
|
295
339
|
# handling.
|
|
296
340
|
for rule in rules:
|
|
297
|
-
if _match_rule(rule, user_msg, text, model, estimated_tokens, has_tools):
|
|
341
|
+
if _match_rule(rule, user_msg, text, model, estimated_tokens, has_tools, hints):
|
|
298
342
|
_emit_resolved(rule, user_msg, text, model, estimated_tokens, has_tools)
|
|
299
343
|
return rule.profile
|
|
300
344
|
|
|
@@ -194,12 +194,13 @@ def strip_thinking(request: AnthropicRequest) -> AnthropicRequest:
|
|
|
194
194
|
|
|
195
195
|
# model_dump() serializes extras; roundtripping via validate rebuilds a
|
|
196
196
|
# clean instance without the dropped key. exclude=True fields (profile,
|
|
197
|
-
# anthropic_beta) are omitted by model_dump, so we reassign them.
|
|
197
|
+
# anthropic_beta, session_key) are omitted by model_dump, so we reassign them.
|
|
198
198
|
dumped = request.model_dump()
|
|
199
199
|
dumped.pop("thinking", None)
|
|
200
200
|
stripped = AnthropicRequest.model_validate(dumped)
|
|
201
201
|
stripped.profile = request.profile
|
|
202
202
|
stripped.anthropic_beta = request.anthropic_beta
|
|
203
|
+
stripped.session_key = request.session_key
|
|
203
204
|
return stripped
|
|
204
205
|
|
|
205
206
|
|
|
@@ -450,6 +451,7 @@ def emulate_tool_choice(request: AnthropicRequest) -> AnthropicRequest:
|
|
|
450
451
|
emulated = AnthropicRequest.model_validate(dumped)
|
|
451
452
|
emulated.profile = request.profile
|
|
452
453
|
emulated.anthropic_beta = request.anthropic_beta
|
|
454
|
+
emulated.session_key = request.session_key
|
|
453
455
|
return emulated
|
|
454
456
|
|
|
455
457
|
|
|
@@ -522,6 +524,7 @@ def strip_cache_control(request: AnthropicRequest) -> tuple[AnthropicRequest, in
|
|
|
522
524
|
stripped = AnthropicRequest.model_validate(dumped)
|
|
523
525
|
stripped.profile = request.profile
|
|
524
526
|
stripped.anthropic_beta = request.anthropic_beta
|
|
527
|
+
stripped.session_key = request.session_key
|
|
525
528
|
return stripped, removed
|
|
526
529
|
|
|
527
530
|
|