coderouter-cli 2.17.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.17.0 → coderouter_cli-2.18.0}/CHANGELOG.md +37 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/PKG-INFO +1 -1
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/adapters/anthropic_native.py +4 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/adapters/base.py +3 -0
- {coderouter_cli-2.17.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.17.0 → coderouter_cli-2.18.0}/coderouter/cli.py +2 -1
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/config/schemas.py +36 -1
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/doctor_claude_code.py +12 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/ingress/anthropic_routes.py +8 -2
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/ingress/openai_routes.py +7 -2
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/routing/auto_router.py +46 -2
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/routing/capability.py +4 -1
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/translation/anthropic.py +3 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/translation/convert.py +4 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/vscode_init.py +10 -1
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/llamacpp-direct.en.md +21 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/llamacpp-direct.md +21 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/subagent-routing.en.md +19 -2
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/subagent-routing.md +22 -2
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/pyproject.toml +1 -1
- coderouter_cli-2.18.0/tests/test_auto_router_hints.py +99 -0
- {coderouter_cli-2.17.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.17.0 → coderouter_cli-2.18.0}/.gitignore +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/LICENSE +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/README.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/README.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/config/loader.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/cost.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/credentials.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/errors.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/guards/context_budget.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/guards/self_healing.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/ingress/anthropic_errors.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/ingress/launcher_routes.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/ingress/tool_search.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/ingress/ui_auth.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/launcher_devices.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/launcher_speculative.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/launcher_swap.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/logging.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/py.typed +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/rollback.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/routing/fallback.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/routing/fallback_trace.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/secret_redaction.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/state/audit_log.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/state/request_log.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/README.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/README.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/external-agents.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/external-agents.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/gguf_dl.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/hf-ollama-models.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/launcher-quickstart.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/launcher.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/launcher.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/architecture.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/commit-point.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/commit-point.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/context-budget.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/context-budget.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/continuous-probing.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/drift-detection.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/partial-stitch.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/stream-truncation.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/concepts/stream-truncation.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/designs/agent-cli-plugin-extraction.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/designs/external-agents-adapter.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/designs/launcher-model-swap.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/designs/launcher-multi-build.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/designs/orchestration-companion.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/remote-access.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/remote-access.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/security.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/guides/vscode.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/low-memory-integration.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/.env.example +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/README.md +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/providers-agent-cli.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/providers-multiagent.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/providers.cli-session.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/providers.llamacpp-vllm.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/providers.opusplan.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/providers.raspberrypi.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/providers.swap.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/examples/providers.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/__init__.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/conftest.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_agent_cli.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_anthropic_errors.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_auto_router.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_budget.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_capability.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_capability_tool_choice.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_cli.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_cli_bind_warning.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_config.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_config_cwd_warning.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_config_shim_actions.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_credentials.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_doctor_claude_code.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_errors.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_examples_yaml_roundtrip.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fallback_cache_strip.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fallback_empty_response.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fallback_reason.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fallback_tool_choice_emulate.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fallback_tool_integrity.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fix_h1_prometheus_drift.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fix_h2_h6_translation.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fix_h3_client_reuse.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fix_h4_h5_guards.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fix_h7_probe_restore.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fix_h8_launcher_auth.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fix_mA_routing.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fix_mB_adapters.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fix_mC_convert.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fix_mD_logging_metrics.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fix_mE_config_validation.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fix_mF_ingress.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_fix_v213_launcher_hardening.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_import_hygiene.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_ingress_count_tokens.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_backend_variants.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_config_bench.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_devices.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_devices_detect.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_devices_routes.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_gui_devices.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_gui_events.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_gui_readiness.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_gui_sweep_close.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_mtp.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_mtp_fallback.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_provider_sync.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_readiness_restart.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_swap.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_swap_review.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_variant_config.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_variant_gui.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_variant_routes.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_launcher_variant_sweep.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_models_passthrough.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_packaging.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_plugin_adapter.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_rollback.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_secret_redaction.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_stop_reason_forward_compat.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_stream_truncation.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_tool_repair_differential.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_tool_repair_lenient.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_tool_repair_r4.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_tool_repair_scanners.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_tool_search.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_toolrepair_bench.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_translation_reverse.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_ttft_timeout.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_ui_auth.py +0 -0
- {coderouter_cli-2.17.0 → coderouter_cli-2.18.0}/tests/test_vscode_init.py +0 -0
|
@@ -9,6 +9,43 @@ 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
|
+
|
|
12
49
|
## [v2.17.0] — 2026-10-24 (time-to-first-token cut-off, retry signals)
|
|
13
50
|
|
|
14
51
|
### Added
|
|
@@ -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):
|
|
@@ -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
|
)
|
|
@@ -562,6 +562,23 @@ class ProviderConfig(BaseModel):
|
|
|
562
562
|
# withholds those opening events from the client. ``None`` (default)
|
|
563
563
|
# disables it. Typical use: a local llama-server still loading a model
|
|
564
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
|
+
)
|
|
565
582
|
ttft_timeout_s: float | None = Field(
|
|
566
583
|
default=None,
|
|
567
584
|
gt=0.0,
|
|
@@ -1639,6 +1656,21 @@ class RuleMatcher(BaseModel):
|
|
|
1639
1656
|
# declares any tools); ``False`` is rejected by ``_exactly_one``
|
|
1640
1657
|
# since a "no-tools" rule would shadow the default fall-through.
|
|
1641
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
|
|
1642
1674
|
|
|
1643
1675
|
_MATCHER_FIELDS: tuple[str, ...] = (
|
|
1644
1676
|
"has_image",
|
|
@@ -1649,12 +1681,15 @@ class RuleMatcher(BaseModel):
|
|
|
1649
1681
|
"content_token_count_min",
|
|
1650
1682
|
"has_tools",
|
|
1651
1683
|
"cjk_ratio_min",
|
|
1684
|
+
"request_class",
|
|
1685
|
+
"agent_type",
|
|
1686
|
+
"compaction",
|
|
1652
1687
|
)
|
|
1653
1688
|
|
|
1654
1689
|
# Boolean matcher fields carry meaning only at ``True`` — the runtime
|
|
1655
1690
|
# evaluator uses ``is True`` — so a ``False`` value is a dead rule that
|
|
1656
1691
|
# never matches. ``_exactly_one`` rejects it explicitly (see below).
|
|
1657
|
-
_BOOL_MATCHER_FIELDS: tuple[str, ...] = ("has_image", "has_tools")
|
|
1692
|
+
_BOOL_MATCHER_FIELDS: tuple[str, ...] = ("has_image", "has_tools", "compaction")
|
|
1658
1693
|
|
|
1659
1694
|
@model_validator(mode="after")
|
|
1660
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:
|
|
@@ -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:
|
|
@@ -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
|
|
|
@@ -277,6 +277,9 @@ class AnthropicRequest(BaseModel):
|
|
|
277
277
|
# beta-gated body fields like `context_management`, `cache_control`,
|
|
278
278
|
# `thinking` beyond what the default minor version accepts.
|
|
279
279
|
anthropic_beta: str | None = Field(default=None, exclude=True)
|
|
280
|
+
# v2.18.0: conversation key for KV-slot affinity (Claude Code's
|
|
281
|
+
# ``x-claude-code-session-id``). Never serialized.
|
|
282
|
+
session_key: str | None = Field(default=None, exclude=True)
|
|
280
283
|
|
|
281
284
|
@model_validator(mode="before")
|
|
282
285
|
@classmethod
|
|
@@ -285,6 +285,8 @@ def to_chat_request(req: AnthropicRequest) -> ChatRequest:
|
|
|
285
285
|
)
|
|
286
286
|
# Propagate CodeRouter routing hint.
|
|
287
287
|
chat_req.profile = req.profile
|
|
288
|
+
# v2.18.0: carry the KV-slot affinity key across the translation.
|
|
289
|
+
chat_req.session_key = req.session_key
|
|
288
290
|
return chat_req
|
|
289
291
|
|
|
290
292
|
|
|
@@ -1114,6 +1116,8 @@ def to_anthropic_request(chat_req: ChatRequest) -> AnthropicRequest:
|
|
|
1114
1116
|
)
|
|
1115
1117
|
# Propagate CodeRouter routing hint.
|
|
1116
1118
|
req.profile = chat_req.profile
|
|
1119
|
+
# v2.18.0: KV-slot affinity key (exclude=True, so set by hand).
|
|
1120
|
+
req.session_key = chat_req.session_key
|
|
1117
1121
|
return req
|
|
1118
1122
|
|
|
1119
1123
|
|
|
@@ -127,16 +127,25 @@ _MANAGED_ENV_KEYS: tuple[str, ...] = (
|
|
|
127
127
|
# is given. Since 2.1.285 Claude Code assumes a 1M window behind a
|
|
128
128
|
# gateway for Opus 4.7+ / Sonnet 5+ / Fable ids, so auto-compaction would
|
|
129
129
|
# wait far past what a local model holds.
|
|
130
|
+
# * ``CLAUDE_CODE_GATEWAY_HINT_HEADERS=1`` (v2.18.0) — makes Claude Code send
|
|
131
|
+
# ``x-claude-code-request-class`` / ``-agent-type`` / ``-compaction`` …, which
|
|
132
|
+
# the auto router's ``request_class`` / ``agent_type`` / ``compaction``
|
|
133
|
+
# matchers read. Harmless when no rule uses them.
|
|
130
134
|
CLAUDE_CODE_ENV_KEYS: tuple[str, ...] = (
|
|
131
135
|
"ENABLE_TOOL_SEARCH",
|
|
132
136
|
"CLAUDE_CODE_AUTO_MODE_SERVER",
|
|
137
|
+
"CLAUDE_CODE_GATEWAY_HINT_HEADERS",
|
|
133
138
|
"CLAUDE_CODE_MAX_CONTEXT_TOKENS",
|
|
134
139
|
)
|
|
135
140
|
|
|
136
141
|
|
|
137
142
|
def claude_code_env(*, max_context_tokens: int | None = None) -> dict[str, str]:
|
|
138
143
|
"""v2.16.0: the Claude Code env vars ``vscode-init`` writes by default."""
|
|
139
|
-
env = {
|
|
144
|
+
env = {
|
|
145
|
+
"ENABLE_TOOL_SEARCH": "false",
|
|
146
|
+
"CLAUDE_CODE_AUTO_MODE_SERVER": "0",
|
|
147
|
+
"CLAUDE_CODE_GATEWAY_HINT_HEADERS": "1",
|
|
148
|
+
}
|
|
140
149
|
if max_context_tokens is not None:
|
|
141
150
|
if max_context_tokens <= 0:
|
|
142
151
|
raise ValueError("max_context_tokens must be positive")
|
|
@@ -264,3 +264,24 @@ Naming the provider `llamacpp-<model>` keeps things consistent with the conventi
|
|
|
264
264
|
Known issue: combining a nextn-embedded model (or active speculative decoding) with `--split-mode tensor` can crash llama.cpp ([issue #24309](https://github.com/ggml-org/llama.cpp/issues/24309)). Use `--split-mode layer` if this applies to you.
|
|
265
265
|
|
|
266
266
|
**The [CodeRouter Launcher](./launcher.en.md#mtp--speculative-decoding-llamacpp) automates this whole dance** — if the main gguf embeds nextn, or a companion draft/MTP gguf sits in the same folder, the Launcher auto-detects it and adds `--spec-type` / `--model-draft` to the launch command for you (set the **MTP** field to `off` to disable).
|
|
267
|
+
|
|
268
|
+
## Reusing the KV cache per conversation across slots (v2.18.0)
|
|
269
|
+
|
|
270
|
+
`llama-server --parallel N` serves N slots, each with its own prompt cache. Without a hint the server picks a free slot, so the next turn of the same conversation can land on a different slot and prefill the whole prompt again.
|
|
271
|
+
|
|
272
|
+
Set `slot_affinity_slots: N` (the same N as `--parallel`) on the provider and CodeRouter sends `id_slot = hash(session) % N`, so a conversation always lands on the same slot.
|
|
273
|
+
|
|
274
|
+
```yaml
|
|
275
|
+
providers:
|
|
276
|
+
- name: local-llamacpp
|
|
277
|
+
kind: openai_compat
|
|
278
|
+
base_url: http://127.0.0.1:8080/v1
|
|
279
|
+
model: ""
|
|
280
|
+
slot_affinity_slots: 4 # match llama-server --parallel 4
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
- The main conversation and its compaction share a slot. Sub-agents and auxiliary calls (auto-mode classifier, titles, …) get their own key per kind, so they neither wait on the main slot nor evict its cache.
|
|
284
|
+
- The conversation key is `x-claude-code-session-id`, one of Claude Code's hint headers; it needs `CLAUDE_CODE_GATEWAY_HINT_HEADERS=1` (set by `coderouter vscode-init`). Requests without it get nothing added.
|
|
285
|
+
- llama.cpp only — do not set it for other servers (they ignore `id_slot` or reject it as unknown).
|
|
286
|
+
- An `id_slot` set explicitly in `extra_body` wins.
|
|
287
|
+
- With more conversations than slots, conversations share slots (and evict each other's cache).
|
|
@@ -316,3 +316,24 @@ provider name は `llamacpp-<モデル>` の命名で揃えると `examples/prov
|
|
|
316
316
|
既知の問題として、nextn 埋め込みモデル(または speculative decoding 有効時)と `--split-mode tensor` の組み合わせは llama.cpp がクラッシュすることがあります([issue #24309](https://github.com/ggml-org/llama.cpp/issues/24309))。該当する場合は `--split-mode layer` を使ってください。
|
|
317
317
|
|
|
318
318
|
**この一連の手組みは [CodeRouter Launcher](./launcher.md#mtp--speculative-decoding-llamacpp) が自動化しています** — メインの gguf に nextn が埋め込まれているか、同じフォルダに companion の draft/MTP gguf があれば、Launcher が `--spec-type` / `--model-draft` を自動検出して起動コマンドに付与します(`MTP` 欄で `off` にすれば無効化も可能)。
|
|
319
|
+
|
|
320
|
+
## 複数スロットで会話ごとに KV キャッシュを使い回す(v2.18.0)
|
|
321
|
+
|
|
322
|
+
`llama-server --parallel N` で起動すると、サーバは N 個のスロットを持ち、それぞれがプロンプトのキャッシュを抱えます。何も指定しないと空いているスロットが選ばれるため、同じ会話の次のターンが別のスロットに行き、プロンプト全体を処理し直すことがあります。
|
|
323
|
+
|
|
324
|
+
provider に `slot_affinity_slots: N`(`--parallel` と同じ数)を書くと、CodeRouter は会話ごとに `id_slot = hash(セッション) % N` を付けて送り、同じ会話は毎回同じスロットに入ります。
|
|
325
|
+
|
|
326
|
+
```yaml
|
|
327
|
+
providers:
|
|
328
|
+
- name: local-llamacpp
|
|
329
|
+
kind: openai_compat
|
|
330
|
+
base_url: http://127.0.0.1:8080/v1
|
|
331
|
+
model: ""
|
|
332
|
+
slot_affinity_slots: 4 # llama-server --parallel 4 と合わせる
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
- 本会話と、その要約(compaction)は同じスロットを使います。サブエージェントや補助呼び出し(auto mode の判定、タイトル生成など)は、種類ごとに別のキーになり、本会話のスロットを待たず、そのキャッシュも上書きしません。
|
|
336
|
+
- 会話の識別には、Claude Code がヒントヘッダで送る `x-claude-code-session-id` を使います。`CLAUDE_CODE_GATEWAY_HINT_HEADERS=1` が必要です(`coderouter vscode-init` が設定します)。ヘッダが無い要求には何も付けません。
|
|
337
|
+
- llama.cpp 専用の設定です。ほかのサーバには付けないでください(無視されるか、未知のパラメータとして拒否されます)。
|
|
338
|
+
- `extra_body` で `id_slot` を明示している場合は、そちらが優先されます。
|
|
339
|
+
- 会話の数がスロット数より多いと、別の会話が同じスロットを共有します(そのたびにキャッシュは入れ替わります)。
|
|
@@ -124,7 +124,7 @@ auto_router:
|
|
|
124
124
|
- `model_pattern` uses **`re.fullmatch`**, not `re.search`. Your regex must match the *entire* string of whatever model name Claude Code actually sends (whether it stays `opus` or expands to `claude-opus-4-8` is environment-dependent). Forgetting the trailing `.*` is a common way for a rule to silently miss.
|
|
125
125
|
- The exact model-name string that arrives is environment- and version-dependent, and cannot be assumed. **Before relying on a rule in production, confirm the real value from the auto-router log** (the `signals.model` field of the `auto-router-resolved` event). See [§6](#6-verifying-it-works) for the procedure.
|
|
126
126
|
|
|
127
|
-
### The full list of auto_router matchers (
|
|
127
|
+
### The full list of auto_router matchers (11 total)
|
|
128
128
|
|
|
129
129
|
A `RuleMatcher` allows **exactly one matcher per rule** — a load-time validator enforces this, so AND-composing multiple conditions is not possible (see gap G1 in [§7](#7-limitations-and-known-gaps)). Rules are evaluated top-to-bottom, first match wins.
|
|
130
130
|
|
|
@@ -139,7 +139,24 @@ A `RuleMatcher` allows **exactly one matcher per rule** — a load-time validato
|
|
|
139
139
|
| `content_token_count_min` | `int` (≥1) | Matches if the estimated token count (char/4 heuristic) over system + all messages is ≥ threshold | Entire request |
|
|
140
140
|
| `has_tools` | `bool` (only `true` is valid) | Matches if the body declares 1+ entries in `tools[]` (OpenAI/Anthropic common) or legacy OpenAI `functions[]` | Entire body |
|
|
141
141
|
|
|
142
|
-
`
|
|
142
|
+
**The hint-header matchers (v2.18.0)** — `request_class` (`x-claude-code-request-class`: `main` / `subagent` / `workflow` / `compaction` / `auxiliary`), `agent_type` (`x-claude-code-agent-type`, e.g. `Explore`) and `compaction: true` (`x-claude-code-compaction`) — work only when Claude Code 2.1.273+ runs with `CLAUDE_CODE_GATEWAY_HINT_HEADERS=1` (set by `coderouter vscode-init`). Unlike the body heuristics, Claude Code itself declares what the request is, so there is nothing to guess. Matching is case-insensitive.
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
auto_router:
|
|
146
|
+
default_rule_profile: main
|
|
147
|
+
rules:
|
|
148
|
+
- id: aux-to-tiny # auto-mode classifier, titles → smallest model
|
|
149
|
+
match: { request_class: auxiliary }
|
|
150
|
+
profile: tiny-local
|
|
151
|
+
- id: compaction-long-ctx # conversation summaries → long-context model
|
|
152
|
+
match: { compaction: true }
|
|
153
|
+
profile: long-context
|
|
154
|
+
- id: explore-fast # Explore sub-agents → fast model
|
|
155
|
+
match: { agent_type: Explore }
|
|
156
|
+
profile: fast-local
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`model_pattern`, `content_token_count_min`, and `has_tools` can fire even without a user message (e.g. a system-only request, or a body carrying just `model` + `tools`). Every other matcher requires a user message to be present. Boolean matchers (`has_image` / `has_tools` / `compaction`) reject an explicit `false` at load time (dead-rule prevention — omit the field instead if unused).
|
|
143
160
|
|
|
144
161
|
> **`content_token_count_min` means something very different as of v2.12.0.** The estimator now counts `tool_result`, `tool_use` and `thinking` blocks, which v2.11.x treated as zero characters, so the same conversation estimates an order of magnitude higher. Measured on a tool-driven session: v2.11.x reported 1,083 tokens after 200 turns; v2.12.0 reports 93,183. **An existing threshold will fire far earlier than you tuned it for** — including the `32000` used in the examples below. Retune it, or set `token_estimation_include_tool_content: false` to restore the v2.11.x estimate. See [context-budget.md](../concepts/context-budget.en.md).
|
|
145
162
|
|
|
@@ -124,7 +124,7 @@ auto_router:
|
|
|
124
124
|
- `model_pattern` は `re.search` ではなく **`re.fullmatch`** である。Claude Code が実際に送るモデル名文字列(`opus` のままか `claude-opus-4-8` に展開されるかは環境依存)の**全体**にマッチする正規表現を書く必要がある。末尾の `.*` を忘れると一致しない。
|
|
125
125
|
- 実際に届くモデル名は環境・バージョン依存で断定できない。**運用前に必ず auto-router のログ(`auto-router-resolved` イベントの `signals.model`)で実値を確認する**こと。確認手順は [§6 動作確認](#6-動作確認) を参照。
|
|
126
126
|
|
|
127
|
-
### auto_router matcher 一覧(全
|
|
127
|
+
### auto_router matcher 一覧(全 11 種)
|
|
128
128
|
|
|
129
129
|
`RuleMatcher` は **1 ルール = ちょうど 1 matcher**。読み込み時のバリデータが強制するため、複数条件の AND 合成はできない(→ [§7](#7-制限と既知の課題) の G1)。ルール順は「先着一致」で、上から順に評価される。
|
|
130
130
|
|
|
@@ -138,8 +138,28 @@ auto_router:
|
|
|
138
138
|
| `model_pattern` | `str` | body の `model` に対する `re.fullmatch`(読み込み時にコンパイル検証) | body の `model` |
|
|
139
139
|
| `content_token_count_min` | `int`(≥1) | 全メッセージ(system + messages)の推定トークン数(char/4)が閾値以上 | リクエスト全体 |
|
|
140
140
|
| `has_tools` | `bool`(`true` のみ有効) | `tools[]`(OpenAI/Anthropic 共通)または legacy `functions[]` を1個以上宣言 | body 全体 |
|
|
141
|
+
| `request_class`(v2.18.0) | `str` | Claude Code のヒントヘッダ `x-claude-code-request-class`(`main` / `subagent` / `workflow` / `compaction` / `auxiliary`)と一致(大小区別なし) | ヘッダ |
|
|
142
|
+
| `agent_type`(v2.18.0) | `str` | `x-claude-code-agent-type`(例: `Explore`)と一致(大小区別なし) | ヘッダ |
|
|
143
|
+
| `compaction`(v2.18.0) | `bool`(`true` のみ有効) | `x-claude-code-compaction` が真 | ヘッダ |
|
|
141
144
|
|
|
142
|
-
|
|
145
|
+
**ヒントヘッダの matcher(v2.18.0)** は、Claude Code 2.1.273 以降に `CLAUDE_CODE_GATEWAY_HINT_HEADERS=1` を設定したときだけ働きます(`coderouter vscode-init` が設定します)。本文から推測する他の matcher と違い、リクエストが何であるか(本会話か、サブエージェントか、要約か、権限判定などの補助呼び出しか)を Claude Code 自身が申告するので、取り違えがありません。たとえば次のように使います。
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
auto_router:
|
|
149
|
+
default_rule_profile: main
|
|
150
|
+
rules:
|
|
151
|
+
- id: aux-to-tiny # auto mode の判定やタイトル生成は最小モデルへ
|
|
152
|
+
match: { request_class: auxiliary }
|
|
153
|
+
profile: tiny-local
|
|
154
|
+
- id: compaction-long-ctx # 会話の要約は文脈の長いモデルへ
|
|
155
|
+
match: { compaction: true }
|
|
156
|
+
profile: long-context
|
|
157
|
+
- id: explore-fast # Explore サブエージェントは速いモデルへ
|
|
158
|
+
match: { agent_type: Explore }
|
|
159
|
+
profile: fast-local
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`model_pattern` / `content_token_count_min` / `has_tools` は user メッセージが無くても発火できる(system-only なリクエストや model+tools のみの body)。それ以外の matcher は user メッセージが必須。ブール matcher(`has_image` / `has_tools` / `compaction`)に `false` を明示すると読み込みエラーになる(死にルール防止のため、未使用は省略する)。
|
|
143
163
|
|
|
144
164
|
> **v2.12.0 で `content_token_count_min` の実効値が大きく変わりました。** 推定器が `tool_result` / `tool_use` / `thinking` を数えるようになったため(v2.11.x は 0 文字扱い)、同じ会話でも推定値が桁で増えます。実測では tool 主体のセッションで、v2.11.x なら 200 ターンでも 1,083 tokens だったものが v2.12.0 では 93,183 tokens です。**既存の閾値を書いている場合、想定よりずっと早くルールが発火します。** 本ページ以下の例で使っている `32000` も同様です。閾値を見直すか、`token_estimation_include_tool_content: false` で v2.11.x の推定に戻してください。詳細は [context-budget.md](../concepts/context-budget.md) を参照。
|
|
145
165
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# in plan.md §11.B; once granted, this name will become an alias and
|
|
12
12
|
# `coderouter` will become the canonical distribution name.
|
|
13
13
|
name = "coderouter-cli"
|
|
14
|
-
version = "2.
|
|
14
|
+
version = "2.18.0"
|
|
15
15
|
description = "Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible."
|
|
16
16
|
readme = "README.md"
|
|
17
17
|
requires-python = ">=3.12"
|