coderouter-cli 2.7.0__tar.gz → 2.7.1__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.7.0 → coderouter_cli-2.7.1}/CHANGELOG.md +51 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/PKG-INFO +28 -9
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/README.md +27 -8
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/config/capability_registry.py +22 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/config/schemas.py +78 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/ingress/anthropic_routes.py +108 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/logging.py +5 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/routing/capability.py +217 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/routing/fallback.py +152 -0
- coderouter_cli-2.7.1/coderouter/translation/tool_repair.py +898 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/pyproject.toml +1 -1
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_capability_degraded_payload.py +6 -4
- coderouter_cli-2.7.1/tests/test_capability_tool_choice.py +251 -0
- coderouter_cli-2.7.1/tests/test_config_shim_actions.py +49 -0
- coderouter_cli-2.7.1/tests/test_fallback_cache_strip.py +254 -0
- coderouter_cli-2.7.1/tests/test_fallback_tool_choice_emulate.py +267 -0
- coderouter_cli-2.7.1/tests/test_ingress_count_tokens.py +150 -0
- coderouter_cli-2.7.1/tests/test_tool_repair_lenient.py +388 -0
- coderouter_cli-2.7.0/coderouter/translation/tool_repair.py +0 -293
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/.gitignore +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/LICENSE +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/README.en.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/adapters/anthropic_native.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/adapters/base.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/adapters/openai_compat.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/cli.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/config/loader.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/cost.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/errors.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/guards/context_budget.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/guards/self_healing.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/ingress/launcher_routes.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/ingress/openai_routes.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/state/audit_log.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/state/request_log.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/coderouter/translation/convert.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/README.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/backends/launcher.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/backends/llamacpp-direct.en.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/backends/llamacpp-direct.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/concepts/context-budget.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/guides/security.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/examples/.env.example +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/examples/README.md +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/examples/providers-multiagent.yaml +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/examples/providers.llamacpp-vllm.yaml +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/examples/providers.raspberrypi.yaml +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/examples/providers.yaml +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/__init__.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/conftest.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_auto_router.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_budget.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_capability.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_cli.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_config.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_context_budget.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_errors.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fix_h1_prometheus_drift.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fix_h2_h6_translation.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fix_h3_client_reuse.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fix_h4_h5_guards.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fix_h7_probe_restore.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fix_h8_launcher_auth.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fix_mA_routing.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fix_mB_adapters.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fix_mC_convert.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fix_mD_logging_metrics.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fix_mE_config_validation.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_fix_mF_ingress.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.7.0 → coderouter_cli-2.7.1}/tests/test_translation_reverse.py +0 -0
|
@@ -6,6 +6,57 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [v2.7.1] — 2026-07-04 (Native-endpoint shims + benchmarked tool-repair upgrade)
|
|
10
|
+
|
|
11
|
+
Two feature PRs driven by the 2026-07-04 competitive re-survey and the new
|
|
12
|
+
tool-call repair benchmark: **PR #39** (native `/v1/messages` endpoint gap
|
|
13
|
+
shims) and **PR #40** (lenient tool-call repair). All new behavior is
|
|
14
|
+
**opt-in with `off` defaults** (except the previously-404 `count_tokens`
|
|
15
|
+
endpoint, which is additive), no config-schema breaking changes, and the
|
|
16
|
+
Core runtime stays at **5 dependencies**.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`POST /v1/messages/count_tokens`** (was 404). Anthropic-shape
|
|
21
|
+
`{"input_tokens": N}`; accurate when the provider declares
|
|
22
|
+
`tokenizer_path`, char/4 heuristic otherwise. Fills a gap native backends
|
|
23
|
+
leave open (Ollama's Anthropic-compatible API ships without
|
|
24
|
+
`count_tokens` / `tool_choice` / prompt caching). (PR #39)
|
|
25
|
+
- **`tool_choice_action: off | warn | emulate`** (profile option) +
|
|
26
|
+
`Capabilities.tool_choice` + registry field + `provider_supports_tool_choice()`.
|
|
27
|
+
`emulate` rewrites forced tool_choice (`any` / `tool`) into a system-prompt
|
|
28
|
+
instruction for non-supporting backends; the original request is preserved
|
|
29
|
+
for fallback to capable providers. New capability-degraded reason:
|
|
30
|
+
`unsupported-backend`. (PR #39)
|
|
31
|
+
- **`cache_control_action: off | strip`** (profile option). `strip` removes
|
|
32
|
+
`cache_control` markers before sending to non-supporting providers
|
|
33
|
+
(logged as `cache-control-stripped`; deliberately no tokens-saved
|
|
34
|
+
accounting). (PR #39)
|
|
35
|
+
- **Lenient tool-call repair** (`translation/tool_repair.py`): second-pass
|
|
36
|
+
parsing for malformed JSON (double braces, trailing commas, single quotes,
|
|
37
|
+
unquoted keys), key aliases (`tool`/`tool_name`, `parameters`/`input`/`args`),
|
|
38
|
+
and XML-flavoured forms — all gated on `allowed_tool_names`. Residue
|
|
39
|
+
cleanup (empty fences, `[,]`). Benchmark: recall 80.6% → **100%** on a
|
|
40
|
+
55-case corpus; live fail-rate at default temperature 17% → **1%**
|
|
41
|
+
(qwen2.5-coder:7b × 100 requests). (PR #40)
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **Two real false positives in the previous repairer** (python-fence dict
|
|
46
|
+
literals and documentation JSON with allowed tool names were converted
|
|
47
|
+
into executable calls — same class as the v2.7.0 code-eating regression).
|
|
48
|
+
Now fixed and gated by an expanded 12-case negative corpus. (PR #40)
|
|
49
|
+
|
|
50
|
+
### Docs
|
|
51
|
+
|
|
52
|
+
- README repositioned for the native `/v1/messages` era: repair + guards
|
|
53
|
+
lead, wire translation demoted to last (passthrough note); new section
|
|
54
|
+
"why a router when Ollama connects directly?".
|
|
55
|
+
|
|
56
|
+
Tests: **1481 passed, 0 failed** (1401 → 1481; +37 shim, +43 lenient-repair).
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
9
60
|
## [v2.7.0] — 2026-07-02 (Reliability & security: full-source review fixes)
|
|
10
61
|
|
|
11
62
|
22 reliability and security fixes from a full-source review (26,600 lines),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.1
|
|
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
|
|
@@ -70,8 +70,8 @@ Description-Content-Type: text/markdown
|
|
|
70
70
|
│
|
|
71
71
|
▼
|
|
72
72
|
┌─ CodeRouter ─┐
|
|
73
|
-
│
|
|
74
|
-
│
|
|
73
|
+
│ 修復 + ガード │──→ ① ローカル (Ollama — 無料・最速)
|
|
74
|
+
│ 監視 + 診断 │──→ ② 無料クラウド (OpenRouter / NIM)
|
|
75
75
|
│ 自動フォールバック │──→ ③ 有料 (Claude — opt-in 時のみ)
|
|
76
76
|
└──────────────┘
|
|
77
77
|
```
|
|
@@ -79,13 +79,31 @@ Description-Content-Type: text/markdown
|
|
|
79
79
|
**やってくれること:**
|
|
80
80
|
|
|
81
81
|
- ローカルモデルが壊した tool calling を Claude Code に届く前に修復する
|
|
82
|
+
- 8 時間回しても止まらないように 6 種類のガードで守る
|
|
82
83
|
- 1 つ目が落ちたら自動で次のプロバイダに切り替える
|
|
83
84
|
- 有料 API は明示的に許可したときだけ使う (デフォルトは無料のみ)
|
|
84
|
-
- 8 時間回しても止まらないように 6 種類のガードで守る
|
|
85
85
|
- 何がおかしいか `coderouter doctor` コマンド一発で診断する
|
|
86
86
|
|
|
87
87
|
---
|
|
88
88
|
|
|
89
|
+
## 「Ollama に直結できるのに、なぜルーター?」
|
|
90
|
+
|
|
91
|
+
2026 年、Ollama (v0.14+) / LM Studio (0.4.1+) / llama.cpp / vLLM は Anthropic 互換 `/v1/messages` を標準装備しました。`ANTHROPIC_BASE_URL` を直接向ければ Claude Code は一応動きます。
|
|
92
|
+
|
|
93
|
+
**でも直結では、こうなります:**
|
|
94
|
+
|
|
95
|
+
| 直結の現実 | CodeRouter 経由 |
|
|
96
|
+
|---|---|
|
|
97
|
+
| 壊れた tool call は壊れたまま届く | 届く前に修復 |
|
|
98
|
+
| backend が落ちたらセッション終了 | ローカル → 無料 → 有料へ自動フォールバック |
|
|
99
|
+
| 長時間で context 溢れ・drift・ループ | 6 系統ガード + self-healing |
|
|
100
|
+
| モデル名がハードコード (リタイアで即エラー) | プロファイルで抽象化、差し替え 1 行 |
|
|
101
|
+
| 何が悪いか分からない | `doctor` 6 プローブ + `/dashboard` + audit/replay |
|
|
102
|
+
|
|
103
|
+
直結で困っていないなら CodeRouter は不要です。**長時間・無人・弱いモデル**のどれかに当てはまったら、戻ってきてください。
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
89
107
|
## インストール (3 行)
|
|
90
108
|
|
|
91
109
|
```bash
|
|
@@ -120,8 +138,9 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
|
|
|
120
138
|
|
|
121
139
|
| あなたの状況 | CodeRouter は? |
|
|
122
140
|
|---|---|
|
|
123
|
-
| Claude Code + ローカル Ollama で tool calling が壊れる | **必須** —
|
|
124
|
-
| Claude Code + ローカルで長時間回すと止まる |
|
|
141
|
+
| Claude Code + ローカル Ollama で tool calling が壊れる | **必須** — tool 修復 (+ 必要なら wire 変換) |
|
|
142
|
+
| Claude Code + ローカルで長時間回すと止まる | **必須級** — 6 系統ガード + self-healing |
|
|
143
|
+
| Ollama v0.14+ / LM Studio にネイティブ直結で動いてる | **便利** — 直結に無い fallback / ガード / 診断を追加 (passthrough で翻訳ゼロ) |
|
|
125
144
|
| codex / gemini-cli + Ollama 直繋ぎで動いてる | オプション — フォールバックが欲しいなら |
|
|
126
145
|
| Claude API を直接叩いてて問題ない | 不要 |
|
|
127
146
|
|
|
@@ -131,14 +150,14 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
|
|
|
131
150
|
|
|
132
151
|
## 主な機能
|
|
133
152
|
|
|
134
|
-
###
|
|
153
|
+
### 修復と接続
|
|
135
154
|
|
|
136
155
|
| 機能 | 何をしてくれるか |
|
|
137
156
|
|---|---|
|
|
138
|
-
| **Wire 翻訳** | Claude Code (Anthropic形式) ↔ Ollama (OpenAI形式) を自動変換 |
|
|
139
157
|
| **Tool-call 修復** | ローカルモデルがテキストで吐いた JSON を正しい tool_use ブロックに復元 |
|
|
140
158
|
| **3 層フォールバック** | ローカル → 無料クラウド → 有料の順に自動切替 |
|
|
141
|
-
| **出力フィルタ** | `<think>` タグ漏れ、stop marker
|
|
159
|
+
| **出力フィルタ** | `<think>` タグ漏れ、stop marker 漏れ、byte-fallback (`<0xNN>`) を自動除去/修復 |
|
|
160
|
+
| **Wire 翻訳** | Anthropic 形式 ↔ OpenAI 形式を自動変換 (ネイティブ `/v1/messages` 対応 backend は passthrough で翻訳ゼロ) |
|
|
142
161
|
|
|
143
162
|
### 長時間運用ガード
|
|
144
163
|
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
│
|
|
26
26
|
▼
|
|
27
27
|
┌─ CodeRouter ─┐
|
|
28
|
-
│
|
|
29
|
-
│
|
|
28
|
+
│ 修復 + ガード │──→ ① ローカル (Ollama — 無料・最速)
|
|
29
|
+
│ 監視 + 診断 │──→ ② 無料クラウド (OpenRouter / NIM)
|
|
30
30
|
│ 自動フォールバック │──→ ③ 有料 (Claude — opt-in 時のみ)
|
|
31
31
|
└──────────────┘
|
|
32
32
|
```
|
|
@@ -34,13 +34,31 @@
|
|
|
34
34
|
**やってくれること:**
|
|
35
35
|
|
|
36
36
|
- ローカルモデルが壊した tool calling を Claude Code に届く前に修復する
|
|
37
|
+
- 8 時間回しても止まらないように 6 種類のガードで守る
|
|
37
38
|
- 1 つ目が落ちたら自動で次のプロバイダに切り替える
|
|
38
39
|
- 有料 API は明示的に許可したときだけ使う (デフォルトは無料のみ)
|
|
39
|
-
- 8 時間回しても止まらないように 6 種類のガードで守る
|
|
40
40
|
- 何がおかしいか `coderouter doctor` コマンド一発で診断する
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
44
|
+
## 「Ollama に直結できるのに、なぜルーター?」
|
|
45
|
+
|
|
46
|
+
2026 年、Ollama (v0.14+) / LM Studio (0.4.1+) / llama.cpp / vLLM は Anthropic 互換 `/v1/messages` を標準装備しました。`ANTHROPIC_BASE_URL` を直接向ければ Claude Code は一応動きます。
|
|
47
|
+
|
|
48
|
+
**でも直結では、こうなります:**
|
|
49
|
+
|
|
50
|
+
| 直結の現実 | CodeRouter 経由 |
|
|
51
|
+
|---|---|
|
|
52
|
+
| 壊れた tool call は壊れたまま届く | 届く前に修復 |
|
|
53
|
+
| backend が落ちたらセッション終了 | ローカル → 無料 → 有料へ自動フォールバック |
|
|
54
|
+
| 長時間で context 溢れ・drift・ループ | 6 系統ガード + self-healing |
|
|
55
|
+
| モデル名がハードコード (リタイアで即エラー) | プロファイルで抽象化、差し替え 1 行 |
|
|
56
|
+
| 何が悪いか分からない | `doctor` 6 プローブ + `/dashboard` + audit/replay |
|
|
57
|
+
|
|
58
|
+
直結で困っていないなら CodeRouter は不要です。**長時間・無人・弱いモデル**のどれかに当てはまったら、戻ってきてください。
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
44
62
|
## インストール (3 行)
|
|
45
63
|
|
|
46
64
|
```bash
|
|
@@ -75,8 +93,9 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
|
|
|
75
93
|
|
|
76
94
|
| あなたの状況 | CodeRouter は? |
|
|
77
95
|
|---|---|
|
|
78
|
-
| Claude Code + ローカル Ollama で tool calling が壊れる | **必須** —
|
|
79
|
-
| Claude Code + ローカルで長時間回すと止まる |
|
|
96
|
+
| Claude Code + ローカル Ollama で tool calling が壊れる | **必須** — tool 修復 (+ 必要なら wire 変換) |
|
|
97
|
+
| Claude Code + ローカルで長時間回すと止まる | **必須級** — 6 系統ガード + self-healing |
|
|
98
|
+
| Ollama v0.14+ / LM Studio にネイティブ直結で動いてる | **便利** — 直結に無い fallback / ガード / 診断を追加 (passthrough で翻訳ゼロ) |
|
|
80
99
|
| codex / gemini-cli + Ollama 直繋ぎで動いてる | オプション — フォールバックが欲しいなら |
|
|
81
100
|
| Claude API を直接叩いてて問題ない | 不要 |
|
|
82
101
|
|
|
@@ -86,14 +105,14 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
|
|
|
86
105
|
|
|
87
106
|
## 主な機能
|
|
88
107
|
|
|
89
|
-
###
|
|
108
|
+
### 修復と接続
|
|
90
109
|
|
|
91
110
|
| 機能 | 何をしてくれるか |
|
|
92
111
|
|---|---|
|
|
93
|
-
| **Wire 翻訳** | Claude Code (Anthropic形式) ↔ Ollama (OpenAI形式) を自動変換 |
|
|
94
112
|
| **Tool-call 修復** | ローカルモデルがテキストで吐いた JSON を正しい tool_use ブロックに復元 |
|
|
95
113
|
| **3 層フォールバック** | ローカル → 無料クラウド → 有料の順に自動切替 |
|
|
96
|
-
| **出力フィルタ** | `<think>` タグ漏れ、stop marker
|
|
114
|
+
| **出力フィルタ** | `<think>` タグ漏れ、stop marker 漏れ、byte-fallback (`<0xNN>`) を自動除去/修復 |
|
|
115
|
+
| **Wire 翻訳** | Anthropic 形式 ↔ OpenAI 形式を自動変換 (ネイティブ `/v1/messages` 対応 backend は passthrough で翻訳ゼロ) |
|
|
97
116
|
|
|
98
117
|
### 長時間運用ガード
|
|
99
118
|
|
|
@@ -115,6 +115,20 @@ class RegistryCapabilities(BaseModel):
|
|
|
115
115
|
"startup check)."
|
|
116
116
|
),
|
|
117
117
|
)
|
|
118
|
+
tool_choice: bool | None = Field(
|
|
119
|
+
default=None,
|
|
120
|
+
description=(
|
|
121
|
+
"S2 (shim): does the upstream honor a forced Anthropic "
|
|
122
|
+
"``tool_choice`` (``{type: any}`` / ``{type: tool}``) on the "
|
|
123
|
+
"wire? When True, the fallback engine's ``tool_choice_action`` "
|
|
124
|
+
"leaves the request untouched. ``None`` (default) = no opinion "
|
|
125
|
+
"→ the gate falls back to ``provider.kind == 'anthropic'``. "
|
|
126
|
+
"``False`` hard-disables even on an anthropic-kind provider "
|
|
127
|
+
"(useful when a specific model regresses). Independent from "
|
|
128
|
+
"``providers.yaml capabilities.tool_choice`` (explicit "
|
|
129
|
+
"per-provider opt-in, highest precedence)."
|
|
130
|
+
),
|
|
131
|
+
)
|
|
118
132
|
cache_control: bool | None = Field(
|
|
119
133
|
default=None,
|
|
120
134
|
description=(
|
|
@@ -201,6 +215,7 @@ class ResolvedCapabilities:
|
|
|
201
215
|
max_context_tokens: int | None = None
|
|
202
216
|
claude_code_suitability: Literal["ok", "degraded"] | None = None
|
|
203
217
|
cache_control: bool | None = None
|
|
218
|
+
tool_choice: bool | None = None
|
|
204
219
|
|
|
205
220
|
|
|
206
221
|
# ---------------------------------------------------------------------------
|
|
@@ -253,6 +268,7 @@ class CapabilityRegistry:
|
|
|
253
268
|
resolved_max_ctx: int | None = None
|
|
254
269
|
resolved_suitability: Literal["ok", "degraded"] | None = None
|
|
255
270
|
resolved_cache_control: bool | None = None
|
|
271
|
+
resolved_tool_choice: bool | None = None
|
|
256
272
|
|
|
257
273
|
thinking_locked = False
|
|
258
274
|
reasoning_locked = False
|
|
@@ -260,6 +276,7 @@ class CapabilityRegistry:
|
|
|
260
276
|
max_ctx_locked = False
|
|
261
277
|
suitability_locked = False
|
|
262
278
|
cache_control_locked = False
|
|
279
|
+
tool_choice_locked = False
|
|
263
280
|
|
|
264
281
|
for rule in self._rules:
|
|
265
282
|
if not rule.kind_matches(kind):
|
|
@@ -285,6 +302,9 @@ class CapabilityRegistry:
|
|
|
285
302
|
if not cache_control_locked and caps.cache_control is not None:
|
|
286
303
|
resolved_cache_control = caps.cache_control
|
|
287
304
|
cache_control_locked = True
|
|
305
|
+
if not tool_choice_locked and caps.tool_choice is not None:
|
|
306
|
+
resolved_tool_choice = caps.tool_choice
|
|
307
|
+
tool_choice_locked = True
|
|
288
308
|
if (
|
|
289
309
|
thinking_locked
|
|
290
310
|
and reasoning_locked
|
|
@@ -292,6 +312,7 @@ class CapabilityRegistry:
|
|
|
292
312
|
and max_ctx_locked
|
|
293
313
|
and suitability_locked
|
|
294
314
|
and cache_control_locked
|
|
315
|
+
and tool_choice_locked
|
|
295
316
|
):
|
|
296
317
|
break
|
|
297
318
|
|
|
@@ -302,6 +323,7 @@ class CapabilityRegistry:
|
|
|
302
323
|
max_context_tokens=resolved_max_ctx,
|
|
303
324
|
claude_code_suitability=resolved_suitability,
|
|
304
325
|
cache_control=resolved_cache_control,
|
|
326
|
+
tool_choice=resolved_tool_choice,
|
|
305
327
|
)
|
|
306
328
|
|
|
307
329
|
# ------------------------------------------------------------------
|
|
@@ -43,6 +43,18 @@ class Capabilities(BaseModel):
|
|
|
43
43
|
# you explicitly want the raw reasoning text to flow to the client
|
|
44
44
|
# (e.g. CodeRouter is fronting a reasoning-aware downstream).
|
|
45
45
|
reasoning_passthrough: bool = False
|
|
46
|
+
# S2 (shim): Anthropic's ``tool_choice`` forcing modes (``{type: any}``
|
|
47
|
+
# / ``{type: tool, name: ...}``). Only a subset of backends honor a
|
|
48
|
+
# forced tool_choice on the wire; openai_compat translation drops it.
|
|
49
|
+
# This narrow per-model flag mirrors ``thinking`` / ``prompt_cache``:
|
|
50
|
+
# when unset (None), the capability gate falls back to the registry and
|
|
51
|
+
# then to a ``kind == "anthropic"`` heuristic (see
|
|
52
|
+
# ``coderouter/routing/capability.py``). Motivation: let the fallback
|
|
53
|
+
# engine's ``tool_choice_action`` emulate forced tool calls via a
|
|
54
|
+
# system-prompt directive on backends that would otherwise silently
|
|
55
|
+
# ignore the field. Backward compatible — None leaves v2.x behavior
|
|
56
|
+
# untouched (no gate, no emulation).
|
|
57
|
+
tool_choice: bool | None = None
|
|
46
58
|
# v1.0+ fields, declared early so providers.yaml can future-proof
|
|
47
59
|
reasoning_control: Literal["none", "openai", "anthropic", "provider_specific"] = "none"
|
|
48
60
|
mcp: Literal["none", "anthropic", "provider_specific"] = "none"
|
|
@@ -706,6 +718,72 @@ class FallbackChain(BaseModel):
|
|
|
706
718
|
),
|
|
707
719
|
)
|
|
708
720
|
|
|
721
|
+
# --- S2 (shim): tool_choice capability gate + emulation -----------------
|
|
722
|
+
#
|
|
723
|
+
# Anthropic clients (Claude Code, SDKs) can pin the model to a specific
|
|
724
|
+
# tool with ``tool_choice: {type: "tool", name: "X"}`` or force *some*
|
|
725
|
+
# tool with ``{type: "any"}``. Native Anthropic honors this; most
|
|
726
|
+
# openai_compat backends silently ignore it after translation, so the
|
|
727
|
+
# model may answer with plain text where the client expected a tool
|
|
728
|
+
# call. This knob decides what the fallback engine does when a forced
|
|
729
|
+
# tool_choice request is routed to a provider that does not support it
|
|
730
|
+
# (per ``provider_supports_tool_choice``):
|
|
731
|
+
#
|
|
732
|
+
# * ``off`` — no detection / no mutation / no log. Backward-compat
|
|
733
|
+
# default (identical to pre-shim behavior).
|
|
734
|
+
# * ``warn`` — emit a ``capability-degraded`` log line only; the
|
|
735
|
+
# request is sent unchanged.
|
|
736
|
+
# * ``emulate`` — strip the ``tool_choice`` field and inject an English
|
|
737
|
+
# directive into the system prompt instructing the
|
|
738
|
+
# model to call the requested tool. Best-effort
|
|
739
|
+
# forcing for backends without native support. The
|
|
740
|
+
# original request object is left untouched so a later
|
|
741
|
+
# capable provider in the chain still receives the real
|
|
742
|
+
# ``tool_choice``.
|
|
743
|
+
tool_choice_action: Literal["off", "warn", "emulate"] = Field(
|
|
744
|
+
default="off",
|
|
745
|
+
description=(
|
|
746
|
+
"S2 (shim): action when a request carries a forced "
|
|
747
|
+
"``tool_choice`` (``{type: any}`` or ``{type: tool}``) and the "
|
|
748
|
+
"target provider does not support it. ``off`` (default) leaves "
|
|
749
|
+
"the request unchanged. ``warn`` emits a ``capability-degraded`` "
|
|
750
|
+
"log only. ``emulate`` strips ``tool_choice`` and injects a "
|
|
751
|
+
"system-prompt directive to coax the model into calling the "
|
|
752
|
+
"tool. Per-provider — capable providers are never mutated."
|
|
753
|
+
),
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
# --- S3 (shim): cache_control strip -------------------------------------
|
|
757
|
+
#
|
|
758
|
+
# By default cache_control markers are simply lost during Anthropic →
|
|
759
|
+
# OpenAI translation and the ``capability-degraded`` gate logs it
|
|
760
|
+
# (v0.5-B, observability only). Some strict openai_compat backends,
|
|
761
|
+
# however, 400 when an unexpected ``cache_control`` key rides along on a
|
|
762
|
+
# content block (rather than silently ignoring it). ``strip`` proactively
|
|
763
|
+
# removes those keys from a deep copy of the request before dispatch to
|
|
764
|
+
# a non-supporting provider, so the marker never reaches the wire.
|
|
765
|
+
#
|
|
766
|
+
# * ``off`` — legacy behavior: leave the request as-is and rely on
|
|
767
|
+
# the existing ``capability-degraded`` observability log.
|
|
768
|
+
# * ``strip`` — deep-copy the request and remove every ``cache_control``
|
|
769
|
+
# key from system / tools / message blocks before sending
|
|
770
|
+
# to a non-supporting provider; emit a
|
|
771
|
+
# ``cache-control-stripped`` log with the marker count.
|
|
772
|
+
# Does NOT emit a tokens-saved event (this is not token
|
|
773
|
+
# savings, just a wire-compatibility strip).
|
|
774
|
+
cache_control_action: Literal["off", "strip"] = Field(
|
|
775
|
+
default="off",
|
|
776
|
+
description=(
|
|
777
|
+
"S3 (shim): action when a request carries ``cache_control`` "
|
|
778
|
+
"markers and the target provider does not support them. ``off`` "
|
|
779
|
+
"(default) keeps legacy behavior (marker dropped in translation, "
|
|
780
|
+
"``capability-degraded`` log only). ``strip`` removes the "
|
|
781
|
+
"``cache_control`` keys from a deep copy before dispatch and "
|
|
782
|
+
"emits a ``cache-control-stripped`` log. Per-provider — capable "
|
|
783
|
+
"providers are never mutated."
|
|
784
|
+
),
|
|
785
|
+
)
|
|
786
|
+
|
|
709
787
|
@model_validator(mode="after")
|
|
710
788
|
def _check_context_budget_thresholds_ordered(self) -> FallbackChain:
|
|
711
789
|
"""v2.0-F (mE): cross-field sanity of the context-budget ratios.
|
|
@@ -38,6 +38,8 @@ from coderouter.routing import (
|
|
|
38
38
|
NoProvidersAvailableError,
|
|
39
39
|
)
|
|
40
40
|
from coderouter.routing.auto_router import RESERVED_PROFILE_NAME, classify
|
|
41
|
+
from coderouter.token_estimation import extract_text_from_anthropic_request
|
|
42
|
+
from coderouter.token_estimation_accurate import count_tokens, is_accuracy_available
|
|
41
43
|
from coderouter.translation import (
|
|
42
44
|
AnthropicRequest,
|
|
43
45
|
AnthropicStreamEvent,
|
|
@@ -316,6 +318,112 @@ async def messages(
|
|
|
316
318
|
return anth_resp.model_dump(exclude_none=True)
|
|
317
319
|
|
|
318
320
|
|
|
321
|
+
def _resolve_count_tokens_tokenizer_path(
|
|
322
|
+
config: Any, profile: str | None
|
|
323
|
+
) -> str | None:
|
|
324
|
+
"""S1 (shim): best-effort resolve a local tokenizer.json for counting.
|
|
325
|
+
|
|
326
|
+
Routing for ``count_tokens`` is not a full chain resolution — we only
|
|
327
|
+
need *a* representative tokenizer for the profile the request would use.
|
|
328
|
+
We take the first provider of the resolved profile (or the default
|
|
329
|
+
profile) and return its ``tokenizer_path`` when declared. Any resolution
|
|
330
|
+
hiccup (unknown profile, empty chain, stub config in tests) returns
|
|
331
|
+
``None``, which makes :func:`count_tokens` fall back to the char/4
|
|
332
|
+
heuristic — a graceful degrade rather than an error.
|
|
333
|
+
"""
|
|
334
|
+
try:
|
|
335
|
+
chosen = profile or config.default_profile
|
|
336
|
+
chain_cfg = config.profile_by_name(chosen)
|
|
337
|
+
for pname in getattr(chain_cfg, "providers", []) or []:
|
|
338
|
+
pconf = next((p for p in config.providers if p.name == pname), None)
|
|
339
|
+
if pconf is not None:
|
|
340
|
+
tok = getattr(pconf, "tokenizer_path", None)
|
|
341
|
+
if tok:
|
|
342
|
+
return tok
|
|
343
|
+
# First provider resolved but declares no tokenizer — stop
|
|
344
|
+
# here (don't scan the whole chain for an unrelated one).
|
|
345
|
+
return None
|
|
346
|
+
except (AttributeError, KeyError, ValueError, TypeError):
|
|
347
|
+
return None
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@router.post("/messages/count_tokens", response_model=None)
|
|
352
|
+
async def count_tokens_route(
|
|
353
|
+
payload: dict[str, Any],
|
|
354
|
+
request: Request,
|
|
355
|
+
x_coderouter_profile: str | None = Header(default=None, alias=_PROFILE_HEADER),
|
|
356
|
+
x_coderouter_mode: str | None = Header(default=None, alias=_MODE_HEADER),
|
|
357
|
+
) -> dict[str, Any]:
|
|
358
|
+
"""Anthropic ``POST /v1/messages/count_tokens`` — local token estimate.
|
|
359
|
+
|
|
360
|
+
Mirrors Anthropic's count_tokens endpoint: the request body has the
|
|
361
|
+
same shape as ``/v1/messages`` (minus the ``max_tokens`` requirement)
|
|
362
|
+
and the response is ``{"input_tokens": N}``. CodeRouter answers this
|
|
363
|
+
entirely locally — there is no upstream round-trip — using the same
|
|
364
|
+
text-extraction the language-tax / context-budget guards use, feeding
|
|
365
|
+
an accurate local tokenizer when the routed provider declares one and
|
|
366
|
+
the ``accuracy`` extra is installed, otherwise the char/4 heuristic.
|
|
367
|
+
|
|
368
|
+
Validation is deliberately looser than :class:`AnthropicRequest` (no
|
|
369
|
+
``max_tokens`` needed): ``model`` and a non-empty ``messages`` list are
|
|
370
|
+
required, everything else optional. Profile selection follows the same
|
|
371
|
+
body > profile-header > mode-header precedence as ``/messages`` so the
|
|
372
|
+
tokenizer resolves against the provider the real request would hit.
|
|
373
|
+
"""
|
|
374
|
+
config = request.app.state.config
|
|
375
|
+
|
|
376
|
+
if not isinstance(payload, dict):
|
|
377
|
+
raise HTTPException(status_code=400, detail="request body must be a JSON object")
|
|
378
|
+
|
|
379
|
+
messages = payload.get("messages")
|
|
380
|
+
if not isinstance(messages, list) or not messages:
|
|
381
|
+
raise HTTPException(
|
|
382
|
+
status_code=400,
|
|
383
|
+
detail="'messages' is required and must be a non-empty list",
|
|
384
|
+
)
|
|
385
|
+
if "model" not in payload:
|
|
386
|
+
raise HTTPException(status_code=400, detail="'model' is required")
|
|
387
|
+
|
|
388
|
+
# Profile selection — body field wins over header, mode header last
|
|
389
|
+
# (same policy as /messages). Kept minimal: we only need the profile to
|
|
390
|
+
# pick a representative tokenizer, so an unknown mode/profile degrades
|
|
391
|
+
# to the default rather than 400'ing the count.
|
|
392
|
+
profile = payload.get("profile") or x_coderouter_profile
|
|
393
|
+
if profile is None and x_coderouter_mode:
|
|
394
|
+
try:
|
|
395
|
+
profile = config.resolve_mode(x_coderouter_mode)
|
|
396
|
+
except (KeyError, AttributeError):
|
|
397
|
+
profile = None
|
|
398
|
+
|
|
399
|
+
# Combine system + messages (+ tool JSON length) into one text blob and
|
|
400
|
+
# count. tools contribute their JSON length as a coarse proxy for the
|
|
401
|
+
# schema tokens Anthropic would bill.
|
|
402
|
+
text = extract_text_from_anthropic_request(
|
|
403
|
+
system=payload.get("system"),
|
|
404
|
+
messages=messages,
|
|
405
|
+
)
|
|
406
|
+
tools = payload.get("tools")
|
|
407
|
+
if isinstance(tools, list) and tools:
|
|
408
|
+
with contextlib.suppress(TypeError, ValueError):
|
|
409
|
+
text = f"{text}\n{json.dumps(tools, ensure_ascii=False)}"
|
|
410
|
+
|
|
411
|
+
tokenizer_path = _resolve_count_tokens_tokenizer_path(config, profile)
|
|
412
|
+
input_tokens = count_tokens(text, tokenizer_path=tokenizer_path)
|
|
413
|
+
# Report the method count_tokens *actually* used: the accurate backend
|
|
414
|
+
# only engages when a path is declared AND the optional ``tokenizers``
|
|
415
|
+
# dependency is importable (see token_estimation_accurate). Otherwise
|
|
416
|
+
# count_tokens transparently falls back to char/4 — so we must too, or
|
|
417
|
+
# the log would misreport a heuristic count as tokenizer-accurate.
|
|
418
|
+
method = "tokenizer" if (tokenizer_path and is_accuracy_available()) else "heuristic"
|
|
419
|
+
|
|
420
|
+
logger.info(
|
|
421
|
+
"count-tokens-served",
|
|
422
|
+
extra={"method": method, "input_tokens": input_tokens},
|
|
423
|
+
)
|
|
424
|
+
return {"input_tokens": input_tokens}
|
|
425
|
+
|
|
426
|
+
|
|
319
427
|
async def _anthropic_sse_iterator(
|
|
320
428
|
engine: FallbackEngine, anth_req: AnthropicRequest
|
|
321
429
|
) -> AsyncIterator[str]:
|
|
@@ -129,6 +129,7 @@ CapabilityDegradedReason = Literal[
|
|
|
129
129
|
"provider-does-not-support",
|
|
130
130
|
"translation-lossy",
|
|
131
131
|
"non-standard-field",
|
|
132
|
+
"unsupported-backend",
|
|
132
133
|
]
|
|
133
134
|
"""Why a capability was degraded.
|
|
134
135
|
|
|
@@ -142,6 +143,10 @@ CapabilityDegradedReason = Literal[
|
|
|
142
143
|
- ``non-standard-field``: upstream emits a field that is not in the spec
|
|
143
144
|
the ingress speaks, so we strip it on the response-side boundary.
|
|
144
145
|
v0.5-C reasoning field.
|
|
146
|
+
- ``unsupported-backend``: the request asks for a semantic the target
|
|
147
|
+
backend does not honor (S2 forced ``tool_choice``); with action
|
|
148
|
+
``warn`` the request is sent unchanged, with ``emulate`` a separate
|
|
149
|
+
``tool-choice-emulated`` line records the substituted directive.
|
|
145
150
|
"""
|
|
146
151
|
|
|
147
152
|
|