coderouter-cli 2.11.2__tar.gz → 2.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/CHANGELOG.md +204 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/PKG-INFO +1 -1
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/config/schemas.py +50 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/context_budget.py +165 -20
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/launcher_routes.py +11 -1
- coderouter_cli-2.12.0/coderouter/token_estimation.py +441 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/context-budget.en.md +39 -2
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/context-budget.md +39 -2
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/subagent-routing.en.md +2 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/subagent-routing.md +2 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers-multiagent.yaml +9 -1
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.raspberrypi.yaml +26 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.yaml +11 -2
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/pyproject.toml +37 -2
- coderouter_cli-2.12.0/tests/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/conftest.py +23 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_auto_router.py +75 -0
- coderouter_cli-2.12.0/tests/test_context_budget.py +877 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_ingress_count_tokens.py +135 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_backend_variants.py +5 -1
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_gui_devices.py +5 -4
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_gui_readiness.py +4 -3
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_variant_gui.py +7 -4
- coderouter_cli-2.12.0/tests/test_packaging.py +245 -0
- coderouter_cli-2.12.0/tests/test_token_estimation.py +456 -0
- coderouter_cli-2.11.2/coderouter/token_estimation.py +0 -208
- coderouter_cli-2.11.2/tests/test_context_budget.py +0 -404
- coderouter_cli-2.11.2/tests/test_token_estimation.py +0 -135
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/.gitignore +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/LICENSE +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/README.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/README.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/adapters/anthropic_native.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/adapters/base.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/adapters/openai_compat.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/cli.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/config/loader.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/cost.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/errors.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/self_healing.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/anthropic_routes.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/ingress/openai_routes.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/launcher_devices.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/launcher_speculative.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/launcher_swap.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/logging.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/plugins/registry.py +0 -0
- /coderouter_cli-2.11.2/tests/__init__.py → /coderouter_cli-2.12.0/coderouter/py.typed +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/routing/capability.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/routing/fallback.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/state/audit_log.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/state/request_log.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/translation/convert.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/coderouter/vscode_init.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/README.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/README.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/external-agents.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/external-agents.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/gguf_dl.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/hf-ollama-models.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/launcher-quickstart.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/launcher.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/launcher.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/llamacpp-direct.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/llamacpp-direct.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/architecture.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/continuous-probing.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/drift-detection.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/partial-stitch.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/agent-cli-plugin-extraction.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/external-agents-adapter.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/launcher-model-swap.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/launcher-multi-build.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/orchestration-companion.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/remote-access.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/remote-access.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/security.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/guides/vscode.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/low-memory-integration.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/.env.example +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/README.md +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers-agent-cli.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.llamacpp-vllm.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/examples/providers.swap.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_agent_cli.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_budget.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_capability.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_capability_tool_choice.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_cli.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_cli_bind_warning.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_config.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_config_cwd_warning.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_config_shim_actions.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_errors.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_examples_yaml_roundtrip.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_cache_strip.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_empty_response.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fallback_tool_choice_emulate.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_h1_prometheus_drift.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_h2_h6_translation.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_h3_client_reuse.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_h4_h5_guards.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_h7_probe_restore.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_h8_launcher_auth.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_mA_routing.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_mB_adapters.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_mC_convert.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_mD_logging_metrics.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_mE_config_validation.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_fix_mF_ingress.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_import_hygiene.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_config_bench.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_devices.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_devices_detect.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_devices_routes.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_gui_events.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_mtp.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_mtp_fallback.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_provider_sync.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_readiness_restart.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_swap.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_swap_review.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_variant_config.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_variant_routes.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_launcher_variant_sweep.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_models_passthrough.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_plugin_adapter.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_stop_reason_forward_compat.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_tool_repair_differential.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_tool_repair_lenient.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_tool_repair_r4.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_tool_repair_scanners.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_toolrepair_bench.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_translation_reverse.py +0 -0
- {coderouter_cli-2.11.2 → coderouter_cli-2.12.0}/tests/test_vscode_init.py +0 -0
|
@@ -9,6 +9,210 @@ are kept verbatim where the Japanese text itself is the subject).
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## [v2.12.0] — 2026-08-04 (the context-budget guard actually fires now)
|
|
13
|
+
|
|
14
|
+
**This release changes behaviour you can observe.** The token estimator
|
|
15
|
+
counted `tool_result`, `tool_use` and `thinking` blocks as zero
|
|
16
|
+
characters, which meant the L1 context-budget guard stayed silent on
|
|
17
|
+
exactly the sessions it exists to protect — agent conversations, where
|
|
18
|
+
tool output is most of the context. Fixing the estimator makes the guard
|
|
19
|
+
fire, the auto-router's `content_token_count_min` rules match earlier,
|
|
20
|
+
and `POST /v1/messages/count_tokens` return larger numbers.
|
|
21
|
+
|
|
22
|
+
Nothing was removed and no field changed meaning, so configs load
|
|
23
|
+
unchanged. But if you set `context_budget_action: trim`, or wrote a
|
|
24
|
+
`content_token_count_min` threshold, **read the migration note below
|
|
25
|
+
before upgrading.** `token_estimation_include_tool_content: false`
|
|
26
|
+
restores the v2.11.x estimate exactly.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **The token estimator ignored tool blocks, so the context-budget guard
|
|
31
|
+
never fired for agent sessions.** `_extract_text_from_content` only
|
|
32
|
+
summed `type: "text"` blocks. A `tool_result` carrying 5,000 characters
|
|
33
|
+
of file content counted as 0, and so did a `tool_use` input. In a
|
|
34
|
+
Claude Code shaped session — short assistant text, large tool results —
|
|
35
|
+
that under-counted by 8x at 20 turns, 14x at 50 and **24x at 200**, so
|
|
36
|
+
a conversation genuinely at 96,311 tokens was reported as 4,011 and the
|
|
37
|
+
0.80 / 0.90 thresholds were never approached. `context_budget_action:
|
|
38
|
+
trim` was, in practice, inert for the workload it was written for. The
|
|
39
|
+
estimator now walks blocks by type: `text`, `tool_result` (recursing
|
|
40
|
+
into a list-shaped `content`), `tool_use` (name plus its serialised
|
|
41
|
+
input) and `thinking` are counted. **`image` blocks and
|
|
42
|
+
`redacted_thinking.data` stay at zero** and this is load-bearing — a
|
|
43
|
+
naive `json.dumps` over the whole block would have inflated a session
|
|
44
|
+
holding one 400 KB base64 PNG by 7.1x. Unknown block types are skipped
|
|
45
|
+
rather than raising, and a self-referential structure terminates
|
|
46
|
+
(`token_estimation.py`).
|
|
47
|
+
- **Trimming could empty the conversation entirely.** `_normalize_head`
|
|
48
|
+
drops leading `assistant` messages and `user` messages that carry a
|
|
49
|
+
`tool_result`, because neither is a valid opening turn. When the
|
|
50
|
+
preserved tail was `[assistant(tool_use), user(tool_result), …]` — the
|
|
51
|
+
normal shape of an agent conversation — every message matched and the
|
|
52
|
+
trimmer returned an empty list. `AnthropicRequest.messages` has no
|
|
53
|
+
`min_length`, so pydantic accepted it and the upstream API answered
|
|
54
|
+
400. Measured on the shipped code: 81 messages in, 0 out. This was
|
|
55
|
+
reachable before this release too, but only for text-heavy sessions,
|
|
56
|
+
because tool blocks estimated as zero meant trimming rarely engaged.
|
|
57
|
+
Making the estimator honest would have made it routine. There is now
|
|
58
|
+
no path through the trimmer that returns an empty list for a non-empty
|
|
59
|
+
input (`guards/context_budget.py`).
|
|
60
|
+
- **Restoring a head message could undo the entire trim.** When
|
|
61
|
+
normalisation left no valid opening turn, the trimmer restored the most
|
|
62
|
+
recent clean `user` message it had just dropped. If that message was
|
|
63
|
+
the reason the request was over budget — a pasted file, a log dump —
|
|
64
|
+
restoring it put the request straight back over the window while
|
|
65
|
+
reporting `status: "trimmed"`. On a 200 KB paste followed by 25 tool
|
|
66
|
+
turns: 67,066 tokens in, 67,060 out, against a 32,768 window. Across a
|
|
67
|
+
300-session sweep, **89% of the results that still exceeded the window
|
|
68
|
+
were caused by this restore**, none by the preserve floor. Restoration
|
|
69
|
+
now checks the budget: the most recent *affordable* dropped clean user
|
|
70
|
+
wins, and if none fits, a synthetic
|
|
71
|
+
`[earlier conversation trimmed to fit the context budget]` head is
|
|
72
|
+
used instead. Same fixture: 67,066 → 17,062, inside both the window and
|
|
73
|
+
the trim target. The sweep goes to 0% (`guards/context_budget.py`).
|
|
74
|
+
- **The trim loop was quadratic.** It rebuilt the surviving message list
|
|
75
|
+
and re-scanned every character for each unit it considered dropping.
|
|
76
|
+
With the corrected estimator that is 217 ms of blocked event loop for
|
|
77
|
+
an 801-message conversation, and it is called synchronously from the
|
|
78
|
+
request path. Per-message character counts are now computed once and
|
|
79
|
+
decremented as units are dropped: 217 ms → 4 ms. Units that would free
|
|
80
|
+
nothing are skipped rather than dropped, so the trimmer no longer
|
|
81
|
+
destroys history without reducing the estimate. The set of dropped
|
|
82
|
+
messages is unchanged — verified against the old algorithm across 200
|
|
83
|
+
random conversations (`guards/context_budget.py`).
|
|
84
|
+
- **The readiness probe now targets the `127.0.0.1` literal rather than
|
|
85
|
+
`localhost`.** Both copies of `_backend_ready` fetched
|
|
86
|
+
`http://localhost:<port>/health` while the backends they probe listen
|
|
87
|
+
on IPv4 only (`llama-server --host 127.0.0.1` by default), and the bare
|
|
88
|
+
TCP fallback in the same function already used the literal — so the
|
|
89
|
+
HTTP branch was the odd one out. On a host with no IPv6 stack at all,
|
|
90
|
+
where `localhost` resolves to `::1` and nothing else, this fails every
|
|
91
|
+
attempt with `Address family not supported` and a backend that is up
|
|
92
|
+
and serving never registers. Hardening rather than a reported bug: on a
|
|
93
|
+
host that has `::1`, the connect is refused immediately and the
|
|
94
|
+
resolver falls through to IPv4, so the old form worked. Applied to both
|
|
95
|
+
`ingress/launcher_routes.py` and `launcher_gui.py`, which carry
|
|
96
|
+
independent copies of this function.
|
|
97
|
+
|
|
98
|
+
### Added
|
|
99
|
+
|
|
100
|
+
- **`token_estimation_include_tool_content`** (top level, default
|
|
101
|
+
`true`). Set it to `false` to get the v2.11.x estimate back, byte for
|
|
102
|
+
byte — verified identical across 300 fuzzed request bodies on all three
|
|
103
|
+
estimator entry points. It is an escape hatch for operators who tuned
|
|
104
|
+
thresholds against the old numbers and need time to retune; expect it
|
|
105
|
+
to be removed in a future major. It is deliberately not per-profile:
|
|
106
|
+
the auto-router runs *before* a profile is chosen, and `count_tokens`
|
|
107
|
+
answers a client question that must not depend on which chain would
|
|
108
|
+
have served the request (`config/schemas.py`).
|
|
109
|
+
- **`coderouter/py.typed`.** The package has advertised
|
|
110
|
+
`Typing :: Typed` in its classifiers without shipping a PEP 561
|
|
111
|
+
marker, so type checkers treated it as untyped. Verified present in the
|
|
112
|
+
built wheel (`pyproject.toml`).
|
|
113
|
+
- **Release-time version and changelog checks.** `release.yml` now fails
|
|
114
|
+
the build if the pushed tag does not match `pyproject.toml`'s version,
|
|
115
|
+
or if `CHANGELOG.md` has no section for it — the awk that extracts
|
|
116
|
+
release notes silently falls back to a placeholder otherwise. Skipped
|
|
117
|
+
on `workflow_dispatch`, where `github.ref_name` is a branch.
|
|
118
|
+
|
|
119
|
+
### Changed
|
|
120
|
+
|
|
121
|
+
- **`examples/providers.yaml` and `examples/providers-multiagent.yaml`
|
|
122
|
+
now ship `context_budget_action: warn` instead of `trim`.** README
|
|
123
|
+
tells new users to `curl` the first of those, so shipping `trim`
|
|
124
|
+
together with a newly-working guard would have started deleting
|
|
125
|
+
history in conversations for people who never opted into it. Warn
|
|
126
|
+
first, read the logs, then decide. `docs/concepts/context-budget.md`
|
|
127
|
+
was updated to match.
|
|
128
|
+
- **`thinking` blocks are now counted.** Extended-thinking content is
|
|
129
|
+
replayed to the model on the following turn — it has to be, or tool-use
|
|
130
|
+
signatures break — so it occupies the window exactly like text.
|
|
131
|
+
`redacted_thinking.data` is still not counted: it is opaque ciphertext,
|
|
132
|
+
the same category as base64 image data.
|
|
133
|
+
- **Third-party GitHub Actions are pinned to commit SHAs**, with the
|
|
134
|
+
version in a trailing comment so dependabot still tracks them. The
|
|
135
|
+
publish job holds `id-token: write` for Trusted Publishing and the
|
|
136
|
+
release job holds `contents: write`; a moved tag on any action they
|
|
137
|
+
use is a direct path to the PyPI project.
|
|
138
|
+
- **A macOS CI runner was added and then withdrawn again before this
|
|
139
|
+
release shipped.** It failed six launcher tests immediately, and the
|
|
140
|
+
failures turned out to belong to the test harness rather than the
|
|
141
|
+
product: every test that spawns a real stub process and requires its
|
|
142
|
+
`/health` to answer 200 within the harness's 5.0s
|
|
143
|
+
`readiness_timeout_s` failed, and no others. Spawning, argv
|
|
144
|
+
construction, crash detection, SIGTERM handling, TTL and registry
|
|
145
|
+
behaviour all passed on macOS. That 5.0s budget is 1/60th of the
|
|
146
|
+
shipped default of 300.0s, so it does not represent any real
|
|
147
|
+
deployment. Rather than ship six `xfail`s or a red CI, the runner is
|
|
148
|
+
deferred to a follow-up that raises the harness budget first. The
|
|
149
|
+
reasoning and the re-enable steps are recorded in `ci.yml` next to the
|
|
150
|
+
matrix.
|
|
151
|
+
- **Corrected the note about tkinter on CI**, which v2.11.2 got wrong in
|
|
152
|
+
both directions. The GUI test modules originally said the uv-managed
|
|
153
|
+
Python on CI has no tkinter; v2.11.2 "fixed" that to say it does,
|
|
154
|
+
based on measuring `uv python install 3.12.11` in a Linux container.
|
|
155
|
+
The actual CI runs settle it: the ubuntu runner skips those four files
|
|
156
|
+
and the macOS runner ran them. Tk availability is a property of the
|
|
157
|
+
runner image, so the docstrings no longer assert it in either
|
|
158
|
+
direction — they just say to keep the `importorskip`.
|
|
159
|
+
- **The CVE audit covers every extra.** `uv export` passed only
|
|
160
|
+
`--extra dev`, leaving `accuracy` (tokenizers) and `repair`
|
|
161
|
+
(json-repair) — both installable by users — outside pip-audit's view.
|
|
162
|
+
Now `--all-extras`.
|
|
163
|
+
- **The sdist no longer allow-lists `docs` wholesale.** `only-include`
|
|
164
|
+
named the whole directory, so a local `uv build` swept in
|
|
165
|
+
`docs/inside/` and `docs/articles/` — private notes and article drafts
|
|
166
|
+
that `.gitignore` exists to keep out. A CI build from a clean checkout
|
|
167
|
+
was never affected, since those files are untracked, but a maintainer
|
|
168
|
+
building from a working tree would have shipped them. The list now
|
|
169
|
+
names the public subdirectories, and `release.yml` fails the build if
|
|
170
|
+
either directory reappears in the tarball. Every entry has to be
|
|
171
|
+
git-tracked: `docs/evidence` exists only in a maintainer's working tree
|
|
172
|
+
and made the allowlist claim to ship something a fresh checkout does
|
|
173
|
+
not have. `tests/test_packaging.py` now asserts each listed path
|
|
174
|
+
exists, which is how that was caught.
|
|
175
|
+
|
|
176
|
+
### Migration
|
|
177
|
+
|
|
178
|
+
Only two settings change meaning. If you set neither, upgrading is a
|
|
179
|
+
no-op beyond larger `count_tokens` responses.
|
|
180
|
+
|
|
181
|
+
**`context_budget_action: trim` or `warn`** — the guard was effectively
|
|
182
|
+
inert for tool-heavy sessions and now works. Expect
|
|
183
|
+
`context-budget-warning` in the logs, the
|
|
184
|
+
`X-CodeRouter-Context-Budget` response header, and — under `trim` —
|
|
185
|
+
conversation history actually being dropped, with a
|
|
186
|
+
`[earlier conversation trimmed to fit the context budget]` message
|
|
187
|
+
appearing at the head when nothing else fits. Measured against a 32 K
|
|
188
|
+
window, a tool-driven session now warns around turn 32 and trims around
|
|
189
|
+
turn 36; before, it never did either. Start with `warn` and read the
|
|
190
|
+
logs before enabling `trim`.
|
|
191
|
+
|
|
192
|
+
**`content_token_count_min`** — auto-router rules using it will match far
|
|
193
|
+
earlier. A tool-driven session that reported 1,083 tokens after 200 turns
|
|
194
|
+
under v2.11.x reports 93,183 now, so a `32000` threshold moves from never
|
|
195
|
+
firing to firing around turn 69. Check where such a rule routes:
|
|
196
|
+
`examples/providers.raspberrypi.yaml` sends matching traffic to a cloud
|
|
197
|
+
free tier rather than the local model. Retune the threshold, or set
|
|
198
|
+
`token_estimation_include_tool_content: false` while you do.
|
|
199
|
+
|
|
200
|
+
### Tests
|
|
201
|
+
|
|
202
|
+
- 3 new files, 48 new cases. Suite: 2393 passed, 1 skipped, ruff clean.
|
|
203
|
+
- Each fix has a regression test confirmed to fail against the pre-fix
|
|
204
|
+
tree, including `test_trim_never_returns_empty_messages` (81 in, 0 out
|
|
205
|
+
on the old code) and `test_trim_lands_inside_the_window_across_budgets`,
|
|
206
|
+
which asserts that if a trimmed request is still over the window, the
|
|
207
|
+
preserve floor alone must account for it — structurally forbidding the
|
|
208
|
+
restore branch from being the cause.
|
|
209
|
+
- The incremental trim loop is pinned against the old full-recompute
|
|
210
|
+
implementation across 200 random conversations (identical dropped
|
|
211
|
+
sets), and the opt-out is pinned against v2.11.2 across 300 fuzzed
|
|
212
|
+
bodies.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
12
216
|
## [v2.11.2] — 2026-08-04 (file-write safety and launcher event hygiene)
|
|
13
217
|
|
|
14
218
|
Two fixes from the same review round as v2.11.1, split out because they
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.12.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
|
|
@@ -27,6 +27,7 @@ from pydantic import (
|
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
from coderouter.logging import get_logger
|
|
30
|
+
from coderouter.token_estimation import set_include_tool_content
|
|
30
31
|
|
|
31
32
|
logger = get_logger(__name__)
|
|
32
33
|
|
|
@@ -2306,6 +2307,36 @@ class CodeRouterConfig(BaseModel):
|
|
|
2306
2307
|
),
|
|
2307
2308
|
)
|
|
2308
2309
|
|
|
2310
|
+
# H-5 escape hatch: token-estimation scope.
|
|
2311
|
+
#
|
|
2312
|
+
# Deliberately top-level rather than per-``FallbackChain``, even
|
|
2313
|
+
# though ``context_budget_action`` lives on the chain. The shared
|
|
2314
|
+
# char/4 estimator has four consumers and three of them run with no
|
|
2315
|
+
# profile in hand: the auto-router runs *before* a profile exists
|
|
2316
|
+
# (it is what picks one), ``POST /v1/messages/count_tokens`` answers
|
|
2317
|
+
# a client question that must not depend on which chain would have
|
|
2318
|
+
# served it, and language-tax measurement is profile-agnostic. A
|
|
2319
|
+
# per-profile switch would let the router and the guard disagree
|
|
2320
|
+
# about the size of the very same request — exactly the class of bug
|
|
2321
|
+
# this key exists to let operators back out of.
|
|
2322
|
+
token_estimation_include_tool_content: bool = Field(
|
|
2323
|
+
default=True,
|
|
2324
|
+
description=(
|
|
2325
|
+
"v2.12 (H-5): count ``tool_result`` / ``tool_use`` / "
|
|
2326
|
+
"``thinking`` content blocks in the shared char/4 token "
|
|
2327
|
+
"estimator. True (default) is the correct behavior — up to "
|
|
2328
|
+
"v2.11.x these blocks estimated to 0 chars, under-counting a "
|
|
2329
|
+
"Claude Code style session by 5x at 20 turns and ~29x at 200. "
|
|
2330
|
+
"Image blocks stay at 0 either way. Set to false as an escape "
|
|
2331
|
+
"hatch to restore v2.11.x-identical estimates (and therefore "
|
|
2332
|
+
"v2.11.x context-budget firing, auto-router "
|
|
2333
|
+
"``content_token_count_min`` matching and "
|
|
2334
|
+
"``/v1/messages/count_tokens`` numbers) if the corrected "
|
|
2335
|
+
"estimate disrupts a tuned deployment. Compatibility shim "
|
|
2336
|
+
"only — scheduled for removal in a future release."
|
|
2337
|
+
),
|
|
2338
|
+
)
|
|
2339
|
+
|
|
2309
2340
|
# v2.0-I: Continuous probing — background health checks for idle periods.
|
|
2310
2341
|
continuous_probe: Literal["off", "active"] = Field(
|
|
2311
2342
|
default="off",
|
|
@@ -2816,6 +2847,25 @@ class CodeRouterConfig(BaseModel):
|
|
|
2816
2847
|
)
|
|
2817
2848
|
return self
|
|
2818
2849
|
|
|
2850
|
+
@model_validator(mode="after")
|
|
2851
|
+
def _apply_token_estimation_scope(self) -> CodeRouterConfig:
|
|
2852
|
+
"""Publish ``token_estimation_include_tool_content`` process-wide.
|
|
2853
|
+
|
|
2854
|
+
The char/4 estimator is a dependency-free leaf module that every
|
|
2855
|
+
consumer imports directly as a plain function; there is no
|
|
2856
|
+
object graph to thread a flag through, and three of the four
|
|
2857
|
+
consumers have no profile/config handle at their call site. So
|
|
2858
|
+
the loaded config pushes the setting into the module once, here.
|
|
2859
|
+
|
|
2860
|
+
Idempotent and deterministic: the value always reflects the most
|
|
2861
|
+
recently constructed config, which in a server process is the
|
|
2862
|
+
single config that was loaded at startup. Declared last so a
|
|
2863
|
+
config that fails any other validator never takes effect. Tests
|
|
2864
|
+
that flip it should restore the default (True) afterwards.
|
|
2865
|
+
"""
|
|
2866
|
+
set_include_tool_content(self.token_estimation_include_tool_content)
|
|
2867
|
+
return self
|
|
2868
|
+
|
|
2819
2869
|
def provider_by_name(self, name: str) -> ProviderConfig:
|
|
2820
2870
|
"""Look up a provider config by name. Raises KeyError if not found."""
|
|
2821
2871
|
for p in self.providers:
|
|
@@ -38,6 +38,11 @@ Uses the shared :func:`~coderouter.token_estimation.estimate_tokens_from_anthrop
|
|
|
38
38
|
(char/4 heuristic, 5-deps invariant). See that module's docstring
|
|
39
39
|
for the CJK caveat and recommended threshold compensation.
|
|
40
40
|
|
|
41
|
+
Since H-5 the estimator also counts ``tool_result`` / ``tool_use`` /
|
|
42
|
+
``thinking`` blocks, so a Claude-Code-shaped session now reports a
|
|
43
|
+
number 5-29x larger than it did in v2.11.x — i.e. the guard finally
|
|
44
|
+
fires on the sessions it was written for.
|
|
45
|
+
|
|
41
46
|
Trim algorithm
|
|
42
47
|
==============
|
|
43
48
|
|
|
@@ -49,6 +54,16 @@ Trim algorithm
|
|
|
49
54
|
``tool_use`` assistant message (and vice versa).
|
|
50
55
|
5. After removal, re-estimate; if still over ``trim_target``,
|
|
51
56
|
reduce ``preserve_last_n`` by 1 and retry (minimum floor: 2).
|
|
57
|
+
6. The result is **never** an empty message list — see
|
|
58
|
+
:func:`_normalize_head`.
|
|
59
|
+
|
|
60
|
+
Cost
|
|
61
|
+
====
|
|
62
|
+
|
|
63
|
+
The incremental loop pre-computes each message's character count once
|
|
64
|
+
and subtracts as units are dropped, so a trim is O(total chars) rather
|
|
65
|
+
than O(messages x total chars). Before that change an 800-message trim
|
|
66
|
+
blocked the event loop for ~0.2s.
|
|
52
67
|
"""
|
|
53
68
|
|
|
54
69
|
from __future__ import annotations
|
|
@@ -57,6 +72,9 @@ from dataclasses import dataclass
|
|
|
57
72
|
from typing import TYPE_CHECKING, Any
|
|
58
73
|
|
|
59
74
|
from coderouter.token_estimation import (
|
|
75
|
+
chars_to_tokens,
|
|
76
|
+
count_message_chars,
|
|
77
|
+
count_system_chars,
|
|
60
78
|
estimate_tokens_from_anthropic_request,
|
|
61
79
|
)
|
|
62
80
|
|
|
@@ -398,7 +416,37 @@ def _group_removable_units(
|
|
|
398
416
|
return units
|
|
399
417
|
|
|
400
418
|
|
|
401
|
-
def
|
|
419
|
+
def _role_of(msg: Any) -> Any:
|
|
420
|
+
"""Role of a message (Pydantic model or dict)."""
|
|
421
|
+
if hasattr(msg, "role"):
|
|
422
|
+
return msg.role
|
|
423
|
+
if isinstance(msg, dict):
|
|
424
|
+
return msg.get("role")
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _is_clean_user(msg: Any) -> bool:
|
|
429
|
+
"""True for a ``user`` message that does not open with a tool_result."""
|
|
430
|
+
return _role_of(msg) == "user" and not _has_tool_result(msg)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
#: Content of the synthetic head inserted when no real clean ``user``
|
|
434
|
+
#: message can be re-attached within budget. Short on purpose (~17
|
|
435
|
+
#: tokens): its only job is to give Anthropic the valid opening turn the
|
|
436
|
+
#: wire format demands while telling the model why history is missing.
|
|
437
|
+
TRIM_PLACEHOLDER_HEAD_TEXT = "[earlier conversation trimmed to fit the context budget]"
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _synthetic_head() -> dict[str, Any]:
|
|
441
|
+
"""A minimal, always-affordable clean ``user`` message."""
|
|
442
|
+
return {"role": "user", "content": TRIM_PLACEHOLDER_HEAD_TEXT}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _normalize_head(
|
|
446
|
+
messages: list[Any],
|
|
447
|
+
*,
|
|
448
|
+
fallback_head: Any | None = None,
|
|
449
|
+
) -> list[Any]:
|
|
402
450
|
"""Drop leading messages until the first is a clean ``user`` message.
|
|
403
451
|
|
|
404
452
|
Anthropic requires the first message to be a ``user`` message, and a
|
|
@@ -407,18 +455,77 @@ def _normalize_head(messages: list[Any]) -> list[Any]:
|
|
|
407
455
|
any assistant message and any user message that contains a
|
|
408
456
|
tool_result, stopping at the first ``user`` message with no
|
|
409
457
|
tool_result block.
|
|
458
|
+
|
|
459
|
+
**Never returns an empty list for a non-empty input.** In a
|
|
460
|
+
tool-heavy agent session the preserved tail can legitimately look
|
|
461
|
+
like ``[assistant(tool_use), user(tool_result), assistant(tool_use),
|
|
462
|
+
user(tool_result)]`` — every single message matches the drop
|
|
463
|
+
condition, and the naive loop returned ``[]``. ``AnthropicRequest``
|
|
464
|
+
declares no ``min_length`` on ``messages``, so pydantic happily
|
|
465
|
+
forwarded that and the upstream API answered 400, destroying the
|
|
466
|
+
session the guard exists to protect. (Latent before H-5 only
|
|
467
|
+
because ``tool_result`` blocks estimated to 0 chars, so trim never
|
|
468
|
+
fired on exactly those conversations.)
|
|
469
|
+
|
|
470
|
+
When no surviving message can serve as the head, ``fallback_head``
|
|
471
|
+
is prepended. The caller picks it under the token budget — see
|
|
472
|
+
:func:`_pick_fallback_head`; this function deliberately does no
|
|
473
|
+
budget arithmetic of its own.
|
|
410
474
|
"""
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
475
|
+
if not messages:
|
|
476
|
+
return list(messages)
|
|
477
|
+
|
|
478
|
+
for start, msg in enumerate(messages):
|
|
479
|
+
if _is_clean_user(msg):
|
|
480
|
+
return messages[start:]
|
|
481
|
+
|
|
482
|
+
# Every surviving message is an assistant turn or a tool_result-
|
|
483
|
+
# carrying user turn: normalizing by dropping would empty the list.
|
|
484
|
+
if fallback_head is not None:
|
|
485
|
+
return [fallback_head, *messages]
|
|
486
|
+
return list(messages)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _pick_fallback_head(
|
|
490
|
+
messages: list[Any],
|
|
491
|
+
msg_chars: list[int],
|
|
492
|
+
dropped: set[int],
|
|
493
|
+
*,
|
|
494
|
+
before: int,
|
|
495
|
+
kept_chars: int,
|
|
496
|
+
target_tokens: int,
|
|
497
|
+
) -> Any:
|
|
498
|
+
"""Choose a head message that keeps the result inside the budget.
|
|
499
|
+
|
|
500
|
+
Called only when nothing in the surviving window is a clean ``user``
|
|
501
|
+
message, so *something* has to be put in front or the request is a
|
|
502
|
+
guaranteed 400.
|
|
503
|
+
|
|
504
|
+
Selection order:
|
|
505
|
+
|
|
506
|
+
1. The **most recent** dropped clean ``user`` message that still
|
|
507
|
+
fits under ``target_tokens`` — recency matters because it is
|
|
508
|
+
usually the operator's latest standing instruction.
|
|
509
|
+
2. Failing that, a synthetic placeholder (:func:`_synthetic_head`).
|
|
510
|
+
|
|
511
|
+
The budget check is the whole point. Re-attaching the *newest*
|
|
512
|
+
candidate unconditionally is a trap: the most recent clean user
|
|
513
|
+
message in an agent session is very often a huge pasted file, and
|
|
514
|
+
restoring it undoes the entire trim. Measured on the canonical
|
|
515
|
+
"200 KB paste then 25 tool calls" shape, that path turned a
|
|
516
|
+
67,066-token request into a 67,061-token request against a 24,576
|
|
517
|
+
target — reported as ``status="trimmed"`` and then rejected
|
|
518
|
+
upstream. A budget-blind restore does not avoid the 400, it
|
|
519
|
+
guarantees it.
|
|
520
|
+
"""
|
|
521
|
+
for idx in sorted(dropped, reverse=True):
|
|
522
|
+
if idx >= before:
|
|
523
|
+
continue
|
|
524
|
+
if not _is_clean_user(messages[idx]):
|
|
525
|
+
continue
|
|
526
|
+
if chars_to_tokens(kept_chars + msg_chars[idx]) <= target_tokens:
|
|
527
|
+
return messages[idx]
|
|
528
|
+
return _synthetic_head()
|
|
422
529
|
|
|
423
530
|
|
|
424
531
|
def _do_trim(
|
|
@@ -437,12 +544,23 @@ def _do_trim(
|
|
|
437
544
|
hard floor that is never removed. After trimming, the head is
|
|
438
545
|
normalized so the first surviving message is a ``user`` message
|
|
439
546
|
without a leading ``tool_result`` (avoids upstream 400s). Bug H5.
|
|
547
|
+
|
|
548
|
+
The loop is incremental in *characters*: every message's character
|
|
549
|
+
contribution is computed exactly once up front and subtracted as
|
|
550
|
+
units are dropped, instead of rebuilding the kept list and
|
|
551
|
+
re-walking the whole conversation on every iteration (the old
|
|
552
|
+
O(messages x chars) shape cost ~0.2s of event-loop time for an
|
|
553
|
+
800-message session). Because the char/4 heuristic floor-divides
|
|
554
|
+
the grand total exactly once, the running sum stays bit-identical
|
|
555
|
+
to a full recompute.
|
|
440
556
|
"""
|
|
441
557
|
if not messages:
|
|
442
558
|
return messages
|
|
443
559
|
|
|
444
|
-
|
|
445
|
-
|
|
560
|
+
system_chars = count_system_chars(system)
|
|
561
|
+
msg_chars = [count_message_chars(m) for m in messages]
|
|
562
|
+
total_chars = system_chars + sum(msg_chars)
|
|
563
|
+
if chars_to_tokens(total_chars) <= target_tokens:
|
|
446
564
|
# Nothing to do — return the input unchanged.
|
|
447
565
|
return messages
|
|
448
566
|
|
|
@@ -453,14 +571,41 @@ def _do_trim(
|
|
|
453
571
|
# Indices we have decided to drop; peel atomic units from the front.
|
|
454
572
|
dropped: set[int] = set()
|
|
455
573
|
for unit in units:
|
|
456
|
-
|
|
457
|
-
estimated = estimate_tokens_from_anthropic_request(system=system, messages=kept)
|
|
458
|
-
if estimated <= target_tokens:
|
|
574
|
+
if chars_to_tokens(total_chars) <= target_tokens:
|
|
459
575
|
break
|
|
576
|
+
unit_chars = sum(msg_chars[i] for i in unit)
|
|
577
|
+
if unit_chars <= 0:
|
|
578
|
+
# Dropping this unit would not move the estimate at all
|
|
579
|
+
# (e.g. an image-only turn, which counts as 0 chars by
|
|
580
|
+
# design). Destroying history for no budget gain is pure
|
|
581
|
+
# loss — skip it and try the next unit.
|
|
582
|
+
continue
|
|
460
583
|
dropped.update(unit)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
584
|
+
total_chars -= unit_chars
|
|
585
|
+
|
|
586
|
+
kept_indices = [i for i in range(len(messages)) if i not in dropped]
|
|
587
|
+
trimmed = [messages[i] for i in kept_indices]
|
|
588
|
+
if not trimmed:
|
|
589
|
+
# Structural fail-safe: never hand back an empty history.
|
|
590
|
+
return list(messages[-max(preserve_last_n, 1) :])
|
|
591
|
+
|
|
592
|
+
if any(_is_clean_user(m) for m in trimmed):
|
|
593
|
+
# A real head survives — no fallback needed, and computing one
|
|
594
|
+
# would only cost time.
|
|
595
|
+
return _normalize_head(trimmed)
|
|
596
|
+
|
|
597
|
+
# Nothing in the surviving window can open the request. Pick a head
|
|
598
|
+
# that fits the remaining budget (``total_chars`` already reflects
|
|
599
|
+
# every drop above), falling back to a synthetic marker.
|
|
600
|
+
fallback_head = _pick_fallback_head(
|
|
601
|
+
messages,
|
|
602
|
+
msg_chars,
|
|
603
|
+
dropped,
|
|
604
|
+
before=kept_indices[0],
|
|
605
|
+
kept_chars=total_chars,
|
|
606
|
+
target_tokens=target_tokens,
|
|
607
|
+
)
|
|
608
|
+
return _normalize_head(trimmed, fallback_head=fallback_head)
|
|
464
609
|
|
|
465
610
|
|
|
466
611
|
__all__ = [
|
|
@@ -714,11 +714,21 @@ async def _backend_ready(backend: str, port: int, *, probe_timeout_s: float) ->
|
|
|
714
714
|
Falling through to the bare TCP connect here would silently re-introduce
|
|
715
715
|
the bug readiness gating was added to fix (provider registered before the
|
|
716
716
|
model finished loading).
|
|
717
|
+
|
|
718
|
+
The probe targets ``127.0.0.1`` literally, never ``localhost``. On a host
|
|
719
|
+
where ``localhost`` resolves to ``::1`` first — the default on GitHub's
|
|
720
|
+
macOS runners, and common on Macs generally — an httpx request to
|
|
721
|
+
``http://localhost:<port>`` fails with ``Address family not supported``
|
|
722
|
+
against a backend listening on IPv4 only, which is what ``llama-server``
|
|
723
|
+
does by default (``--host 127.0.0.1``). Readiness then never succeeds and
|
|
724
|
+
the spawn times out with ``status='loading'`` even though the backend is
|
|
725
|
+
up and serving. The bare TCP fallback below already used ``127.0.0.1``;
|
|
726
|
+
this branch was the odd one out.
|
|
717
727
|
"""
|
|
718
728
|
if base_backend(backend) in ("llama.cpp", "vllm"):
|
|
719
729
|
try:
|
|
720
730
|
async with httpx.AsyncClient(timeout=probe_timeout_s) as client:
|
|
721
|
-
resp = await client.get(f"http://
|
|
731
|
+
resp = await client.get(f"http://127.0.0.1:{port}/health")
|
|
722
732
|
return resp.status_code == 200
|
|
723
733
|
except Exception:
|
|
724
734
|
return False
|