coderouter-cli 2.6.0__tar.gz → 2.7.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.6.0 → coderouter_cli-2.7.0}/.gitignore +1 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/CHANGELOG.md +182 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/PKG-INFO +32 -1
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/README.en.md +31 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/README.md +31 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/adapters/anthropic_native.py +66 -30
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/adapters/base.py +46 -3
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/adapters/openai_compat.py +53 -11
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/config/schemas.py +154 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/context_budget.py +114 -19
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/continuous_probe.py +9 -1
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/memory_pressure.py +16 -2
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/self_healing.py +14 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/anthropic_routes.py +115 -1
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/app.py +162 -1
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/dashboard_routes.py +23 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/launcher_routes.py +187 -11
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/openai_routes.py +77 -7
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/logging.py +24 -3
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/metrics/collector.py +479 -210
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/metrics/prometheus.py +3 -3
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/routing/fallback.py +387 -27
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/state/audit_log.py +69 -7
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/state/request_log.py +70 -7
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/translation/convert.py +112 -9
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/translation/tool_repair.py +39 -23
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/README.md +38 -1
- coderouter_cli-2.7.0/docs/backends/claude-code-llamacpp-vllm.md +172 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/launcher.md +2 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/concepts/architecture.md +1 -1
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/concepts/drift-detection.md +3 -2
- coderouter_cli-2.7.0/docs/guides/language-tax.en.md +138 -0
- coderouter_cli-2.7.0/docs/guides/language-tax.md +139 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/security.en.md +17 -3
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/security.md +3 -2
- coderouter_cli-2.7.0/examples/README.md +68 -0
- coderouter_cli-2.7.0/examples/providers-multiagent.yaml +188 -0
- coderouter_cli-2.6.0/examples/providers.v2-context-budget.yaml → coderouter_cli-2.7.0/examples/providers.context-budget-test.yaml +2 -1
- coderouter_cli-2.6.0/examples/providers.llama-cpp-vllm.yaml → coderouter_cli-2.7.0/examples/providers.llamacpp-vllm.yaml +19 -9
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/examples/providers.nvidia-nim.yaml +1 -0
- coderouter_cli-2.6.0/examples/providers.auto-custom.yaml → coderouter_cli-2.7.0/examples/providers.ollama-auto-custom.yaml +6 -5
- coderouter_cli-2.6.0/examples/providers.auto.yaml → coderouter_cli-2.7.0/examples/providers.ollama-auto.yaml +2 -1
- coderouter_cli-2.6.0/examples/providers.note-2026.yaml → coderouter_cli-2.7.0/examples/providers.ollama-free-chain.yaml +3 -2
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/examples/providers.raspberrypi.yaml +1 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/examples/providers.yaml +1 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/pyproject.toml +1 -1
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_auto_router.py +9 -11
- coderouter_cli-2.7.0/tests/test_fix_h1_prometheus_drift.py +102 -0
- coderouter_cli-2.7.0/tests/test_fix_h2_h6_translation.py +114 -0
- coderouter_cli-2.7.0/tests/test_fix_h3_client_reuse.py +188 -0
- coderouter_cli-2.7.0/tests/test_fix_h4_h5_guards.py +289 -0
- coderouter_cli-2.7.0/tests/test_fix_h7_probe_restore.py +152 -0
- coderouter_cli-2.7.0/tests/test_fix_h8_launcher_auth.py +316 -0
- coderouter_cli-2.7.0/tests/test_fix_mA_routing.py +616 -0
- coderouter_cli-2.7.0/tests/test_fix_mB_adapters.py +215 -0
- coderouter_cli-2.7.0/tests/test_fix_mC_convert.py +494 -0
- coderouter_cli-2.7.0/tests/test_fix_mD_logging_metrics.py +410 -0
- coderouter_cli-2.7.0/tests/test_fix_mE_config_validation.py +260 -0
- coderouter_cli-2.7.0/tests/test_fix_mF_ingress.py +349 -0
- coderouter_cli-2.7.0/tests/test_tokens_saved_metric.py +87 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/LICENSE +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/cli.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/config/loader.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/cost.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/errors.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/routing/capability.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/llamacpp-direct.en.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/llamacpp-direct.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/concepts/context-budget.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/examples/.env.example +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/__init__.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/conftest.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_budget.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_capability.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_cli.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_config.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_context_budget.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_errors.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.6.0 → coderouter_cli-2.7.0}/tests/test_translation_reverse.py +0 -0
|
@@ -6,6 +6,188 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [v2.7.0] — 2026-07-02 (Reliability & security: full-source review fixes)
|
|
10
|
+
|
|
11
|
+
22 reliability and security fixes from a full-source review (26,600 lines),
|
|
12
|
+
landed as two sequential PRs: high-priority **PR #34** (H1–H8) and
|
|
13
|
+
medium-priority **PR #35** (M1–M14), both merged to `main`. No API or
|
|
14
|
+
config-schema breaking changes. Backward compatibility highlights: the new
|
|
15
|
+
launcher token auth is **opt-in** (`CODEROUTER_LAUNCHER_TOKEN` unset ⇒
|
|
16
|
+
unchanged behavior + a startup warning), and the Host-validation middleware
|
|
17
|
+
allows loopback by default with `CODEROUTER_ALLOWED_HOSTS` to extend it.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **`/metrics` 500 when drift counters are non-zero** — three malformed
|
|
22
|
+
Prometheus label tuples (`(((),), v)` → `((), v)`) (`coderouter/metrics/prometheus.py`, PR #34/H1).
|
|
23
|
+
- **`tool_repair` deleted unrelated fenced code blocks** while stripping
|
|
24
|
+
tool-call-shaped ones, losing user content
|
|
25
|
+
(`coderouter/translation/tool_repair.py`, PR #34/H2).
|
|
26
|
+
- **Probe URL mismatch (`/v1/v1` duplication)** between the continuous probe
|
|
27
|
+
and the adapters caused healthy backends to be wrongly demoted; URL
|
|
28
|
+
normalization is now shared
|
|
29
|
+
(`coderouter/adapters/anthropic_native.py`, `coderouter/guards/continuous_probe.py`, PR #34/H4).
|
|
30
|
+
- **`context_budget` dropped entire history at once** on threshold breach;
|
|
31
|
+
trimming is now sequential per tool-pair with re-estimation after each
|
|
32
|
+
step, and the new head turn is renormalized to a plain user turn
|
|
33
|
+
(`coderouter/guards/context_budget.py`, PR #34/H5).
|
|
34
|
+
- **Empty-stream termination violated the SSE protocol** by sending a
|
|
35
|
+
terminator with no preceding `message_start`; one is now synthesized
|
|
36
|
+
first (`coderouter/translation/convert.py`, PR #34/H6).
|
|
37
|
+
- **Excluded providers never recovered after a restart** — restored
|
|
38
|
+
self-healing state didn't re-arm recovery probes (only the live
|
|
39
|
+
UNHEALTHY-transition path did); probes are now re-spawned after state
|
|
40
|
+
restore, queued if the event loop isn't running yet
|
|
41
|
+
(`coderouter/routing/fallback.py`, `coderouter/guards/self_healing.py`, PR #34/H7).
|
|
42
|
+
- **Malformed upstream responses bypassed retry/fallback** — an uncaught
|
|
43
|
+
pydantic `ValidationError` on missing/misshaped fields now converts to a
|
|
44
|
+
retryable `AdapterError` on non-stream paths, and stream paths skip the
|
|
45
|
+
malformed chunk with a once-per-stream warning
|
|
46
|
+
(`coderouter/adapters/anthropic_native.py`, `coderouter/adapters/openai_compat.py`, PR #35/M6).
|
|
47
|
+
- **`"oom"` substring matched inside `"room"` / `"zoom"` / `"bloom"`** in
|
|
48
|
+
upstream error bodies, cooling down healthy providers; the memory-pressure
|
|
49
|
+
guard now matches the token on word boundaries
|
|
50
|
+
(`coderouter/guards/memory_pressure.py`, PR #35/M10).
|
|
51
|
+
- **Interleaved text/tool-call deltas sent `input_json_delta` to
|
|
52
|
+
already-closed block indices** — blocks are now reopened at a fresh index
|
|
53
|
+
with their original `id`/`name` preserved
|
|
54
|
+
(`coderouter/translation/convert.py`, PR #35/M7).
|
|
55
|
+
- **`tool_result.is_error` was dropped in both translation directions** — an
|
|
56
|
+
`"Error: "` content marker now round-trips the flag without
|
|
57
|
+
double-prefixing (`coderouter/translation/convert.py`, PR #35/M8).
|
|
58
|
+
- **Streams ending without `message_stop` sent no terminal chunk**, hanging
|
|
59
|
+
OpenAI-compatible clients; a `finish_reason` + usage chunk is now
|
|
60
|
+
synthesized on abnormal termination (mirrors the H6 fix in the other
|
|
61
|
+
direction) (`coderouter/translation/convert.py`, PR #35/M9).
|
|
62
|
+
- **`configure_logging` permanently detached the metrics collector** on a
|
|
63
|
+
second `create_app()` call by removing every root handler;
|
|
64
|
+
`install_collector` now re-attaches when missing, and
|
|
65
|
+
`configure_logging` removes only its own marked handlers
|
|
66
|
+
(`coderouter/logging.py`, PR #35/M4).
|
|
67
|
+
- **Metrics persistence save/load asymmetry** zeroed language-tax
|
|
68
|
+
accounting on every restart — `save_state` was missing
|
|
69
|
+
`language_tax_usd` / `language_tax_usd_aggregate`, which `load_state`
|
|
70
|
+
expects (`coderouter/metrics/collector.py`, PR #35/M5).
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- **Config validation now fails fast at load time.** Previously-silent
|
|
75
|
+
misconfiguration is now rejected: profile chains referencing unknown
|
|
76
|
+
provider names, duplicate provider/profile names, inverted thresholds
|
|
77
|
+
(e.g. `context_budget` warn > trim, `trim_target` ≥ `trim_threshold`,
|
|
78
|
+
`recovery_probe_initial_s` > `recovery_probe_max_s`), and
|
|
79
|
+
`has_tools: false` / `has_image: false` matchers (which could never
|
|
80
|
+
match) (`coderouter/config/schemas.py`, PR #35/M13).
|
|
81
|
+
- **Drift-based chain demotion now applies regardless of the `adaptive`
|
|
82
|
+
flag.** A dedicated cooldown-based demotion map is applied in
|
|
83
|
+
`_resolve_anthropic_chain` unconditionally; previously demotion only
|
|
84
|
+
affected ordering when `profile.adaptive` was true (and did nothing
|
|
85
|
+
under five samples) (`coderouter/routing/fallback.py`, PR #35/M3).
|
|
86
|
+
- **Adaptive routing now observes streaming paths.** `record_attempt` was
|
|
87
|
+
only called from `generate_anthropic`; all four entry points
|
|
88
|
+
(`stream_anthropic` / `stream` / `generate` and non-stream) now record
|
|
89
|
+
first-event latency and outcomes, including Claude Code's default
|
|
90
|
+
streaming path (`coderouter/routing/fallback.py`, PR #35/M2).
|
|
91
|
+
- **Drift verdicts are now request-scoped (`ContextVar`)** instead of
|
|
92
|
+
stored on the shared engine, eliminating a race where concurrent
|
|
93
|
+
requests could read another request's drift header or a stale verdict
|
|
94
|
+
during cooldown; the engine attribute remains as a deprecated mirror
|
|
95
|
+
(`coderouter/routing/fallback.py`, PR #35/M1).
|
|
96
|
+
- **Chain resolution and context estimation no longer run twice per
|
|
97
|
+
request** — the resolved dispatch is cached per request and reused when
|
|
98
|
+
the request object is unchanged (`coderouter/routing/fallback.py`, PR #35/M11).
|
|
99
|
+
- **Request/audit logs are now buffered** (20 records or 2 seconds,
|
|
100
|
+
flushed on close/`atexit`; `flush_every_n=1` restores write-through)
|
|
101
|
+
instead of synchronous per-request writes
|
|
102
|
+
(`coderouter/state/audit_log.py`, `coderouter/state/request_log.py`, PR #35/M12).
|
|
103
|
+
- **The metrics collector hot path exits early** for unrecognized events
|
|
104
|
+
before acquiring the lock, and known events are dispatched via a dict
|
|
105
|
+
table instead of a ~30-branch `if`/`elif` chain
|
|
106
|
+
(`coderouter/metrics/collector.py`, PR #35/M12).
|
|
107
|
+
- **Adapters now share a single `httpx.AsyncClient`** (lazily created,
|
|
108
|
+
per-call timeout override, closed on shutdown) instead of creating one
|
|
109
|
+
per request, enabling keep-alive/TLS session reuse
|
|
110
|
+
(`coderouter/adapters/base.py`, `coderouter/adapters/openai_compat.py`,
|
|
111
|
+
`coderouter/adapters/anthropic_native.py`, `coderouter/ingress/app.py`, PR #34/H3).
|
|
112
|
+
|
|
113
|
+
### Security
|
|
114
|
+
|
|
115
|
+
- **Host-validation middleware** on all routes rejects non-loopback `Host`
|
|
116
|
+
headers as a DNS-rebinding defense; loopback
|
|
117
|
+
(`localhost` / `127.0.0.1` / `[::1]`) is allowed by default and
|
|
118
|
+
`CODEROUTER_ALLOWED_HOSTS` (comma-separated) extends the allowlist
|
|
119
|
+
(`coderouter/ingress/app.py`, PR #34/H8).
|
|
120
|
+
- **Opt-in launcher token authentication.** `launcher start/stop/delete`
|
|
121
|
+
require the `X-CodeRouter-Token` header (constant-time compare) when
|
|
122
|
+
`CODEROUTER_LAUNCHER_TOKEN` is set; unset behaves exactly as before, with
|
|
123
|
+
a one-time startup warning (`coderouter/ingress/launcher_routes.py`, PR #34/H8).
|
|
124
|
+
- **`-m`/`--model` re-specification rejected** in launcher `extra_args`/
|
|
125
|
+
`options` to block arbitrary model-path injection
|
|
126
|
+
(`coderouter/ingress/launcher_routes.py`, PR #34/H8).
|
|
127
|
+
- **Request body size cap** (64 MB default, `CODEROUTER_MAX_BODY_BYTES`
|
|
128
|
+
override) returns 413; SSE responses are unaffected
|
|
129
|
+
(`coderouter/ingress/app.py`, PR #35/M14).
|
|
130
|
+
- **`/api/launcher/suggest` path confinement** — resolved paths must now
|
|
131
|
+
fall inside the configured `model_dirs`, closing an arbitrary-path
|
|
132
|
+
existence/size probe (`coderouter/ingress/launcher_routes.py`, PR #35/M14).
|
|
133
|
+
|
|
134
|
+
### Tests
|
|
135
|
+
|
|
136
|
+
- Full suite grew from **1263 → 1401 passed** (1 skipped, environment-only)
|
|
137
|
+
across the two PRs: 139 new regression tests in 12 new test files
|
|
138
|
+
(`tests/test_fix_h*.py` ×6, `tests/test_fix_m*.py` ×6). One existing test,
|
|
139
|
+
`tests/test_auto_router.py::test_has_tools_false_rejected_at_load`, was
|
|
140
|
+
flipped to expect `ValidationError` — its own docstring had anticipated
|
|
141
|
+
this once the new config validator (M13) landed; it is the only existing
|
|
142
|
+
test changed by either PR.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## [v2.6.1] — 2026-06-28 (Token-savings accounting)
|
|
147
|
+
|
|
148
|
+
Patch release: surfaces **token-savings accounting** in the metrics layer
|
|
149
|
+
and dashboard. The figure is owned by core, so it appears even when no
|
|
150
|
+
plugin is installed (trim savings from the context-budget guard), and the
|
|
151
|
+
optional `compress` plugin adds to the same total via a neutral
|
|
152
|
+
`tokens-saved` log event. **No new core dependency**, **no behavioral
|
|
153
|
+
change** to existing paths, and **fully backward compatible** — existing
|
|
154
|
+
counters and events are untouched; the new buckets are additive.
|
|
155
|
+
|
|
156
|
+
### Added
|
|
157
|
+
|
|
158
|
+
- **Token-savings buckets in `MetricsCollector`**
|
|
159
|
+
(`coderouter/metrics/collector.py`). `tokens_saved_total` plus a
|
|
160
|
+
per-mechanism breakdown (`tokens_saved_by_mechanism`). Two feeds
|
|
161
|
+
aggregate under one schema: `trim` (derived from the existing
|
|
162
|
+
`context-budget-trimmed` event's before/after token estimate) and
|
|
163
|
+
`compress` (the neutral `tokens-saved` event emitted by the compress
|
|
164
|
+
plugin — no core import). Wired through `snapshot()`, `save_state()`,
|
|
165
|
+
`load_state()`, and `reset()`.
|
|
166
|
+
- **Dashboard Token Savings tiles**
|
|
167
|
+
(`coderouter/ingress/dashboard_routes.py`). Three tiles — trim /
|
|
168
|
+
compress / total — in the "Cost & Language Tax" panel, zero-filled so a
|
|
169
|
+
fresh or local-only deployment renders cleanly.
|
|
170
|
+
|
|
171
|
+
### Docs / examples
|
|
172
|
+
|
|
173
|
+
- Reorganized provider samples under `examples/` with a category index.
|
|
174
|
+
- Added the language-tax guide (JA/EN) and a Claude Code +
|
|
175
|
+
llama.cpp/vllm backend guide.
|
|
176
|
+
|
|
177
|
+
### Tests
|
|
178
|
+
|
|
179
|
+
- `tests/test_tokens_saved_metric.py` — trim/compress accounting,
|
|
180
|
+
negative-delta clamping, combined aggregate, persistence round-trip,
|
|
181
|
+
and reset. Full suite: **1263 passed, 1 skipped**.
|
|
182
|
+
|
|
183
|
+
### Companion
|
|
184
|
+
|
|
185
|
+
- `coderouter-plugin-compress` emits the `tokens-saved` event (plugin
|
|
186
|
+
branch `feat/tokens-saved-emit`, v0.2.0). CodeRouter core works
|
|
187
|
+
standalone — trim savings show without the plugin.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
9
191
|
## [v2.6.0] — 2026-06-20 (Language Tax: measure, route, visualize)
|
|
10
192
|
|
|
11
193
|
Minor release: makes the CJK **"language tax"** — cloud tokenizers bill
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.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
|
|
@@ -161,6 +161,36 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
|
|
|
161
161
|
| **`coderouter replay`** | provider 切替の効果を統計比較 (A/B 分析) / `--suggest-rules` でルール最適化提案 |
|
|
162
162
|
| **Continuous Probe** | idle 時も定期的に backend を監視 |
|
|
163
163
|
|
|
164
|
+
### 言語税トラッキング — v2.6.0
|
|
165
|
+
|
|
166
|
+
日本語などの CJK テキストは、クラウドのトークナイザだと「同じ意味の英語」より多くのトークンを消費します(**実測: GPT-4o 系 o200k で平均 1.6 倍、GPT-4 系 cl100k で平均 2.0 倍**)。ローカル LLM は課金されないので、この「言語税」はクラウド利用時だけ効いてきます。CodeRouter v2.6.0 はこれを **計測・ルーティング回避・可視化** します。
|
|
167
|
+
|
|
168
|
+
| 機能 | 何をしてくれるか |
|
|
169
|
+
|---|---|
|
|
170
|
+
| **言語税の計測** | プロバイダに `tokenizer_path`(ローカルの `tokenizer.json`)を指定すると、char/4 ヒューリスティック比の実トークン倍率と割増 USD を算出(ネットワーク不要・未設定なら無効) |
|
|
171
|
+
| **`cjk_ratio_min` ルーティング** | CJK 比率が高いリクエストを自動でローカル LLM(課金ゼロ)へ。コードや英語はクラウドへ |
|
|
172
|
+
| **ダッシュボード可視化** | `/dashboard` の「Cost & Language Tax」パネルで総支出・キャッシュ節約・言語税をリアルタイム表示 |
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
# providers.yaml — CJK 多めのターンはローカルへ自動回避
|
|
176
|
+
auto_router:
|
|
177
|
+
rules:
|
|
178
|
+
- match: { cjk_ratio_min: 0.3 } # 日本語が3割以上 → ローカル
|
|
179
|
+
profile: local
|
|
180
|
+
- match: { has_tools: true } # ツール使用 → クラウド
|
|
181
|
+
profile: cloud
|
|
182
|
+
default_rule_profile: cloud
|
|
183
|
+
|
|
184
|
+
providers:
|
|
185
|
+
- name: cloud-sonnet
|
|
186
|
+
kind: anthropic
|
|
187
|
+
base_url: https://api.anthropic.com
|
|
188
|
+
model: claude-sonnet-4-6
|
|
189
|
+
tokenizer_path: ~/.coderouter/tokenizers/sonnet.json # 言語税の正確計測(任意)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
詳細 → [言語税ガイド](./docs/guides/language-tax.md)
|
|
193
|
+
|
|
164
194
|
### Launcher — llama.cpp / vllm 起動 UI
|
|
165
195
|
|
|
166
196
|
`http://localhost:8088/launcher` で開けるブラウザ UI。llama.cpp や vllm を GUI で起動・管理できます。
|
|
@@ -229,6 +259,7 @@ providers:
|
|
|
229
259
|
| 使いこなす | [利用ガイド](./docs/guides/usage-guide.md) |
|
|
230
260
|
| 無料で回す | [無料枠ガイド](./docs/guides/free-tier-guide.md) |
|
|
231
261
|
| llama.cpp / vllm を GUI で起動 | [Launcher ガイド](./docs/backends/launcher.md) |
|
|
262
|
+
| 言語税を計測・回避する | [言語税ガイド](./docs/guides/language-tax.md) |
|
|
232
263
|
| 詰まった | [トラブルシューティング](./docs/guides/troubleshooting.md) |
|
|
233
264
|
| 設計を知りたい | [アーキテクチャ詳細](./docs/concepts/architecture.md) |
|
|
234
265
|
| 全リリース履歴 | [CHANGELOG](./CHANGELOG.md) |
|
|
@@ -116,6 +116,36 @@ Full decision matrix → [Do I need CodeRouter?](./docs/start/when-do-i-need-cod
|
|
|
116
116
|
| **`coderouter replay`** | Compare providers statistically (A/B analysis) / `--suggest-rules` for automated rule suggestions |
|
|
117
117
|
| **Continuous Probe** | Background health monitoring even during idle |
|
|
118
118
|
|
|
119
|
+
### Language Tax tracking — v2.6.0
|
|
120
|
+
|
|
121
|
+
CJK text (Japanese / Chinese / Korean) costs more tokens on cloud tokenizers than the same meaning in English (**measured: ~1.6× on average with GPT-4o-era o200k, ~2.0× with GPT-4-era cl100k**). Local models bill nothing per token, so this "language tax" only bites on the cloud leg. CodeRouter v2.6.0 makes it **measurable, routable, and visible**.
|
|
122
|
+
|
|
123
|
+
| Feature | What it does |
|
|
124
|
+
|---|---|
|
|
125
|
+
| **Language-tax measurement** | Set a provider's `tokenizer_path` (a local `tokenizer.json`) to compute the real token multiplier vs the char/4 heuristic and the extra USD. No network; inert when unset. |
|
|
126
|
+
| **`cjk_ratio_min` routing** | Auto-route CJK-heavy turns to a local (tax-free) model; code/English falls through to the cloud chain. |
|
|
127
|
+
| **Dashboard panel** | The `/dashboard` "Cost & Language Tax" panel shows total spend, cache savings, and language-tax spend live. |
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
# providers.yaml — steer CJK-heavy turns to local
|
|
131
|
+
auto_router:
|
|
132
|
+
rules:
|
|
133
|
+
- match: { cjk_ratio_min: 0.3 } # >=30% CJK chars -> local
|
|
134
|
+
profile: local
|
|
135
|
+
- match: { has_tools: true } # tool use -> cloud
|
|
136
|
+
profile: cloud
|
|
137
|
+
default_rule_profile: cloud
|
|
138
|
+
|
|
139
|
+
providers:
|
|
140
|
+
- name: cloud-sonnet
|
|
141
|
+
kind: anthropic
|
|
142
|
+
base_url: https://api.anthropic.com
|
|
143
|
+
model: claude-sonnet-4-6
|
|
144
|
+
tokenizer_path: ~/.coderouter/tokenizers/sonnet.json # accurate language-tax (optional)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Details → [Language Tax guide](./docs/guides/language-tax.en.md)
|
|
148
|
+
|
|
119
149
|
### Launcher — llama.cpp / vllm GUI
|
|
120
150
|
|
|
121
151
|
Browser UI at `http://localhost:8088/launcher` for starting and managing local inference backends.
|
|
@@ -184,6 +214,7 @@ More detail → [Usage guide](./docs/guides/usage-guide.en.md) · [Architecture]
|
|
|
184
214
|
| Use it well | [Usage guide](./docs/guides/usage-guide.en.md) |
|
|
185
215
|
| Run for free | [Free-tier guide](./docs/guides/free-tier-guide.en.md) |
|
|
186
216
|
| Launch llama.cpp / vllm via GUI | [Launcher guide](./docs/backends/launcher.md) |
|
|
217
|
+
| Measure & avoid the language tax | [Language Tax guide](./docs/guides/language-tax.en.md) |
|
|
187
218
|
| Stuck? | [Troubleshooting](./docs/guides/troubleshooting.en.md) |
|
|
188
219
|
| Understand the design | [Architecture](./docs/concepts/architecture.md) |
|
|
189
220
|
| Full release history | [CHANGELOG](./CHANGELOG.md) |
|
|
@@ -116,6 +116,36 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
|
|
|
116
116
|
| **`coderouter replay`** | provider 切替の効果を統計比較 (A/B 分析) / `--suggest-rules` でルール最適化提案 |
|
|
117
117
|
| **Continuous Probe** | idle 時も定期的に backend を監視 |
|
|
118
118
|
|
|
119
|
+
### 言語税トラッキング — v2.6.0
|
|
120
|
+
|
|
121
|
+
日本語などの CJK テキストは、クラウドのトークナイザだと「同じ意味の英語」より多くのトークンを消費します(**実測: GPT-4o 系 o200k で平均 1.6 倍、GPT-4 系 cl100k で平均 2.0 倍**)。ローカル LLM は課金されないので、この「言語税」はクラウド利用時だけ効いてきます。CodeRouter v2.6.0 はこれを **計測・ルーティング回避・可視化** します。
|
|
122
|
+
|
|
123
|
+
| 機能 | 何をしてくれるか |
|
|
124
|
+
|---|---|
|
|
125
|
+
| **言語税の計測** | プロバイダに `tokenizer_path`(ローカルの `tokenizer.json`)を指定すると、char/4 ヒューリスティック比の実トークン倍率と割増 USD を算出(ネットワーク不要・未設定なら無効) |
|
|
126
|
+
| **`cjk_ratio_min` ルーティング** | CJK 比率が高いリクエストを自動でローカル LLM(課金ゼロ)へ。コードや英語はクラウドへ |
|
|
127
|
+
| **ダッシュボード可視化** | `/dashboard` の「Cost & Language Tax」パネルで総支出・キャッシュ節約・言語税をリアルタイム表示 |
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
# providers.yaml — CJK 多めのターンはローカルへ自動回避
|
|
131
|
+
auto_router:
|
|
132
|
+
rules:
|
|
133
|
+
- match: { cjk_ratio_min: 0.3 } # 日本語が3割以上 → ローカル
|
|
134
|
+
profile: local
|
|
135
|
+
- match: { has_tools: true } # ツール使用 → クラウド
|
|
136
|
+
profile: cloud
|
|
137
|
+
default_rule_profile: cloud
|
|
138
|
+
|
|
139
|
+
providers:
|
|
140
|
+
- name: cloud-sonnet
|
|
141
|
+
kind: anthropic
|
|
142
|
+
base_url: https://api.anthropic.com
|
|
143
|
+
model: claude-sonnet-4-6
|
|
144
|
+
tokenizer_path: ~/.coderouter/tokenizers/sonnet.json # 言語税の正確計測(任意)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
詳細 → [言語税ガイド](./docs/guides/language-tax.md)
|
|
148
|
+
|
|
119
149
|
### Launcher — llama.cpp / vllm 起動 UI
|
|
120
150
|
|
|
121
151
|
`http://localhost:8088/launcher` で開けるブラウザ UI。llama.cpp や vllm を GUI で起動・管理できます。
|
|
@@ -184,6 +214,7 @@ providers:
|
|
|
184
214
|
| 使いこなす | [利用ガイド](./docs/guides/usage-guide.md) |
|
|
185
215
|
| 無料で回す | [無料枠ガイド](./docs/guides/free-tier-guide.md) |
|
|
186
216
|
| llama.cpp / vllm を GUI で起動 | [Launcher ガイド](./docs/backends/launcher.md) |
|
|
217
|
+
| 言語税を計測・回避する | [言語税ガイド](./docs/guides/language-tax.md) |
|
|
187
218
|
| 詰まった | [トラブルシューティング](./docs/guides/troubleshooting.md) |
|
|
188
219
|
| 設計を知りたい | [アーキテクチャ詳細](./docs/concepts/architecture.md) |
|
|
189
220
|
| 全リリース履歴 | [CHANGELOG](./CHANGELOG.md) |
|
|
@@ -35,6 +35,7 @@ from collections.abc import AsyncIterator
|
|
|
35
35
|
from typing import Any
|
|
36
36
|
|
|
37
37
|
import httpx
|
|
38
|
+
from pydantic import ValidationError
|
|
38
39
|
|
|
39
40
|
from coderouter.adapters.base import (
|
|
40
41
|
AdapterError,
|
|
@@ -68,6 +69,26 @@ _RETRYABLE_STATUSES = {404, 408, 425, 429, 500, 502, 503, 504}
|
|
|
68
69
|
_DEFAULT_ANTHROPIC_VERSION = "2023-06-01"
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
def anthropic_messages_url(base_url: str) -> str:
|
|
73
|
+
"""Resolve the ``/v1/messages`` endpoint URL from a ``base_url``.
|
|
74
|
+
|
|
75
|
+
``base_url`` may be given with or without a trailing ``/v1`` — users
|
|
76
|
+
may point it at either ``https://api.anthropic.com`` or
|
|
77
|
+
``https://api.anthropic.com/v1`` (LM Studio and similar servers
|
|
78
|
+
commonly advertise the latter). We normalize by stripping a trailing
|
|
79
|
+
``/v1`` so appending ``/v1/messages`` always yields a valid,
|
|
80
|
+
non-duplicated URL.
|
|
81
|
+
|
|
82
|
+
Shared by :class:`AnthropicAdapter` and the continuous-probe guard so
|
|
83
|
+
both hit the exact same endpoint (see bug H4: a mismatch made the
|
|
84
|
+
probe target ``/v1/v1/messages`` and 404 on healthy backends).
|
|
85
|
+
"""
|
|
86
|
+
base = base_url.rstrip("/")
|
|
87
|
+
if base.endswith("/v1"):
|
|
88
|
+
base = base[: -len("/v1")]
|
|
89
|
+
return f"{base}/v1/messages"
|
|
90
|
+
|
|
91
|
+
|
|
71
92
|
class AnthropicAdapter(BaseAdapter):
|
|
72
93
|
"""Native Anthropic Messages API adapter (passthrough).
|
|
73
94
|
|
|
@@ -87,15 +108,10 @@ class AnthropicAdapter(BaseAdapter):
|
|
|
87
108
|
|
|
88
109
|
``base_url`` may be given with or without a trailing ``/v1`` —
|
|
89
110
|
we normalize by stripping it so appending ``/v1/messages``
|
|
90
|
-
always yields a valid URL.
|
|
111
|
+
always yields a valid URL. Delegates to the shared
|
|
112
|
+
:func:`anthropic_messages_url` helper.
|
|
91
113
|
"""
|
|
92
|
-
|
|
93
|
-
# Users may point base_url at either `https://api.anthropic.com`
|
|
94
|
-
# or `https://api.anthropic.com/v1`. We normalize to the former so
|
|
95
|
-
# we can always append /v1/messages.
|
|
96
|
-
if base.endswith("/v1"):
|
|
97
|
-
base = base[: -len("/v1")]
|
|
98
|
-
return f"{base}/v1/messages"
|
|
114
|
+
return anthropic_messages_url(str(self.config.base_url))
|
|
99
115
|
|
|
100
116
|
def _headers(self, request: AnthropicRequest | None = None) -> dict[str, str]:
|
|
101
117
|
"""Build Anthropic-native HTTP headers, including beta-header forwarding.
|
|
@@ -161,21 +177,21 @@ class AnthropicAdapter(BaseAdapter):
|
|
|
161
177
|
that auth works and the endpoint is reachable.
|
|
162
178
|
"""
|
|
163
179
|
try:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
180
|
+
resp = await self.client().post(
|
|
181
|
+
self._url(),
|
|
182
|
+
headers=self._headers(),
|
|
183
|
+
json={
|
|
184
|
+
"model": self.config.model,
|
|
185
|
+
"max_tokens": 1,
|
|
186
|
+
"messages": [{"role": "user", "content": "ping"}],
|
|
187
|
+
},
|
|
188
|
+
timeout=5.0,
|
|
189
|
+
)
|
|
190
|
+
# 200 is clearly healthy. 4xx auth errors indicate the
|
|
191
|
+
# endpoint is reachable even if the key is bad — still a
|
|
192
|
+
# "the server answered" signal for healthcheck purposes.
|
|
193
|
+
# 5xx is upstream trouble.
|
|
194
|
+
return resp.status_code < 500
|
|
179
195
|
except httpx.HTTPError:
|
|
180
196
|
return False
|
|
181
197
|
|
|
@@ -331,8 +347,9 @@ class AnthropicAdapter(BaseAdapter):
|
|
|
331
347
|
timeout = self.effective_timeout(overrides)
|
|
332
348
|
|
|
333
349
|
try:
|
|
334
|
-
|
|
335
|
-
|
|
350
|
+
resp = await self.client().post(
|
|
351
|
+
url, json=payload, headers=self._headers(request), timeout=timeout
|
|
352
|
+
)
|
|
336
353
|
except httpx.TimeoutException as exc:
|
|
337
354
|
raise AdapterError(
|
|
338
355
|
f"timeout contacting {url}", provider=self.name, retryable=True
|
|
@@ -394,7 +411,22 @@ class AnthropicAdapter(BaseAdapter):
|
|
|
394
411
|
# (future additions like thinking blocks) pass through via
|
|
395
412
|
# extra="allow" on AnthropicResponse.
|
|
396
413
|
data["coderouter_provider"] = self.name
|
|
397
|
-
|
|
414
|
+
# M6: guard the model validation. A 200 body that isn't a valid
|
|
415
|
+
# Anthropic Messages response (missing ``content``/``id``, an
|
|
416
|
+
# error envelope returned with a 200, etc.) would otherwise raise
|
|
417
|
+
# a bare pydantic ValidationError that escapes the engine's
|
|
418
|
+
# AdapterError-based retry/fallback path. Convert to a retryable
|
|
419
|
+
# AdapterError so the chain can fall through (mirrors the
|
|
420
|
+
# invalid-JSON branch above, but retryable — a malformed shape is
|
|
421
|
+
# usually transient upstream noise rather than a config fault).
|
|
422
|
+
try:
|
|
423
|
+
return AnthropicResponse.model_validate(data)
|
|
424
|
+
except ValidationError as exc:
|
|
425
|
+
raise AdapterError(
|
|
426
|
+
f"malformed response shape from upstream: {exc}",
|
|
427
|
+
provider=self.name,
|
|
428
|
+
retryable=True,
|
|
429
|
+
) from exc
|
|
398
430
|
|
|
399
431
|
async def stream_anthropic(
|
|
400
432
|
self,
|
|
@@ -424,10 +456,14 @@ class AnthropicAdapter(BaseAdapter):
|
|
|
424
456
|
logged_flag: list[bool] = [False]
|
|
425
457
|
|
|
426
458
|
try:
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
459
|
+
# H3: stream over the shared client (pool / keep-alive / TLS
|
|
460
|
+
# reuse). Only the ``stream(...)`` context is scoped here; the
|
|
461
|
+
# client persists and is closed via ``aclose`` on shutdown. The
|
|
462
|
+
# ``async with`` releases the response's borrowed connection even
|
|
463
|
+
# if the caller abandons this generator mid-stream — no GC leak.
|
|
464
|
+
async with self.client().stream(
|
|
465
|
+
"POST", url, json=payload, headers=self._headers(request), timeout=timeout
|
|
466
|
+
) as resp:
|
|
431
467
|
if resp.status_code >= 400:
|
|
432
468
|
body = await resp.aread()
|
|
433
469
|
raise AdapterError(
|
|
@@ -11,6 +11,7 @@ from abc import ABC, abstractmethod
|
|
|
11
11
|
from collections.abc import AsyncIterator
|
|
12
12
|
from typing import Any, Literal
|
|
13
13
|
|
|
14
|
+
import httpx
|
|
14
15
|
from pydantic import BaseModel, ConfigDict, Field
|
|
15
16
|
|
|
16
17
|
from coderouter.config.schemas import ProviderConfig
|
|
@@ -164,17 +165,59 @@ class BaseAdapter(ABC):
|
|
|
164
165
|
def __init__(self, config: ProviderConfig) -> None:
|
|
165
166
|
"""Bind the adapter to a :class:`ProviderConfig`.
|
|
166
167
|
|
|
167
|
-
Subclasses do not need to override this
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
Subclasses do not need to override this. A single shared
|
|
169
|
+
:class:`httpx.AsyncClient` is created lazily on first use (see
|
|
170
|
+
:meth:`client`) and reused across every call on this adapter so
|
|
171
|
+
the connection pool, HTTP keep-alive, and TLS session are all
|
|
172
|
+
reused. Per-call timeouts are still honored by passing
|
|
173
|
+
``timeout=`` to ``client.post`` / ``client.stream`` — they do
|
|
174
|
+
NOT require a fresh client.
|
|
170
175
|
"""
|
|
171
176
|
self.config = config
|
|
177
|
+
# Shared client, created lazily inside the running event loop the
|
|
178
|
+
# first time an adapter method needs it (see ``client``). Kept as
|
|
179
|
+
# ``None`` until then so constructing an adapter has no I/O cost and
|
|
180
|
+
# is safe outside an event loop (e.g. at import / config time).
|
|
181
|
+
self._client: httpx.AsyncClient | None = None
|
|
172
182
|
|
|
173
183
|
@property
|
|
174
184
|
def name(self) -> str:
|
|
175
185
|
"""Shortcut for ``self.config.name`` — used in log trails and errors."""
|
|
176
186
|
return self.config.name
|
|
177
187
|
|
|
188
|
+
# ---- H3: shared HTTP client (connection-pool / keep-alive reuse) ----
|
|
189
|
+
def client(self) -> httpx.AsyncClient:
|
|
190
|
+
"""Return the shared :class:`httpx.AsyncClient`, creating it lazily.
|
|
191
|
+
|
|
192
|
+
The client is created on first use so construction happens inside
|
|
193
|
+
the running event loop and carries no I/O cost at adapter-build
|
|
194
|
+
time. Subsequent calls return the same instance, which is what
|
|
195
|
+
lets httpx reuse pooled connections, keep-alive, and the TLS
|
|
196
|
+
session across requests.
|
|
197
|
+
|
|
198
|
+
No default timeout is baked in here: every call site passes an
|
|
199
|
+
explicit per-call ``timeout=`` (resolved from the active profile
|
|
200
|
+
via :meth:`effective_timeout`), so leaving the client timeout
|
|
201
|
+
unset avoids a surprising default clamping long-running calls.
|
|
202
|
+
"""
|
|
203
|
+
if self._client is None:
|
|
204
|
+
self._client = httpx.AsyncClient(timeout=None)
|
|
205
|
+
return self._client
|
|
206
|
+
|
|
207
|
+
async def aclose(self) -> None:
|
|
208
|
+
"""Close the shared HTTP client and drop the reference.
|
|
209
|
+
|
|
210
|
+
Idempotent: safe to call when no client was ever created, and
|
|
211
|
+
safe to call more than once. Invoked from the app lifespan
|
|
212
|
+
shutdown path so pooled connections are released cleanly rather
|
|
213
|
+
than left to garbage collection. After ``aclose`` a later call
|
|
214
|
+
re-creates the client on demand via :meth:`client`.
|
|
215
|
+
"""
|
|
216
|
+
client = self._client
|
|
217
|
+
self._client = None
|
|
218
|
+
if client is not None:
|
|
219
|
+
await client.aclose()
|
|
220
|
+
|
|
178
221
|
# ---- v0.6-B override resolution helpers -----------------------------
|
|
179
222
|
def effective_timeout(self, overrides: ProviderCallOverrides | None) -> float:
|
|
180
223
|
"""Profile override wins when set; else provider default."""
|