coderouter-cli 2.6.1__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.6.1 → coderouter_cli-2.7.1}/CHANGELOG.md +188 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/PKG-INFO +28 -9
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/README.md +27 -8
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/adapters/anthropic_native.py +66 -30
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/adapters/base.py +46 -3
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/adapters/openai_compat.py +53 -11
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/config/capability_registry.py +22 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/config/schemas.py +232 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/context_budget.py +114 -19
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/continuous_probe.py +9 -1
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/memory_pressure.py +16 -2
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/self_healing.py +14 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/anthropic_routes.py +223 -1
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/app.py +162 -1
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/launcher_routes.py +187 -11
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/openai_routes.py +77 -7
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/logging.py +29 -3
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/metrics/collector.py +454 -245
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/metrics/prometheus.py +3 -3
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/routing/capability.py +217 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/routing/fallback.py +539 -27
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/state/audit_log.py +69 -7
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/state/request_log.py +70 -7
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/translation/convert.py +112 -9
- coderouter_cli-2.7.1/coderouter/translation/tool_repair.py +898 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/launcher.md +2 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/concepts/architecture.md +1 -1
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/concepts/drift-detection.md +3 -2
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/security.en.md +17 -3
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/security.md +3 -2
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/pyproject.toml +1 -1
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_auto_router.py +9 -11
- {coderouter_cli-2.6.1 → 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_fix_h1_prometheus_drift.py +102 -0
- coderouter_cli-2.7.1/tests/test_fix_h2_h6_translation.py +114 -0
- coderouter_cli-2.7.1/tests/test_fix_h3_client_reuse.py +188 -0
- coderouter_cli-2.7.1/tests/test_fix_h4_h5_guards.py +289 -0
- coderouter_cli-2.7.1/tests/test_fix_h7_probe_restore.py +152 -0
- coderouter_cli-2.7.1/tests/test_fix_h8_launcher_auth.py +316 -0
- coderouter_cli-2.7.1/tests/test_fix_mA_routing.py +616 -0
- coderouter_cli-2.7.1/tests/test_fix_mB_adapters.py +215 -0
- coderouter_cli-2.7.1/tests/test_fix_mC_convert.py +494 -0
- coderouter_cli-2.7.1/tests/test_fix_mD_logging_metrics.py +410 -0
- coderouter_cli-2.7.1/tests/test_fix_mE_config_validation.py +260 -0
- coderouter_cli-2.7.1/tests/test_fix_mF_ingress.py +349 -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.6.1/coderouter/translation/tool_repair.py +0 -277
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/.gitignore +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/LICENSE +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/README.en.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/cli.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/config/loader.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/cost.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/errors.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/README.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/llamacpp-direct.en.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/llamacpp-direct.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/concepts/context-budget.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/.env.example +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/README.md +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers-multiagent.yaml +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.llamacpp-vllm.yaml +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.raspberrypi.yaml +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/examples/providers.yaml +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/__init__.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/conftest.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_budget.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_capability.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_cli.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_config.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_context_budget.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_errors.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.6.1 → coderouter_cli-2.7.1}/tests/test_translation_reverse.py +0 -0
|
@@ -6,6 +6,194 @@ 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
|
+
|
|
60
|
+
## [v2.7.0] — 2026-07-02 (Reliability & security: full-source review fixes)
|
|
61
|
+
|
|
62
|
+
22 reliability and security fixes from a full-source review (26,600 lines),
|
|
63
|
+
landed as two sequential PRs: high-priority **PR #34** (H1–H8) and
|
|
64
|
+
medium-priority **PR #35** (M1–M14), both merged to `main`. No API or
|
|
65
|
+
config-schema breaking changes. Backward compatibility highlights: the new
|
|
66
|
+
launcher token auth is **opt-in** (`CODEROUTER_LAUNCHER_TOKEN` unset ⇒
|
|
67
|
+
unchanged behavior + a startup warning), and the Host-validation middleware
|
|
68
|
+
allows loopback by default with `CODEROUTER_ALLOWED_HOSTS` to extend it.
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
|
|
72
|
+
- **`/metrics` 500 when drift counters are non-zero** — three malformed
|
|
73
|
+
Prometheus label tuples (`(((),), v)` → `((), v)`) (`coderouter/metrics/prometheus.py`, PR #34/H1).
|
|
74
|
+
- **`tool_repair` deleted unrelated fenced code blocks** while stripping
|
|
75
|
+
tool-call-shaped ones, losing user content
|
|
76
|
+
(`coderouter/translation/tool_repair.py`, PR #34/H2).
|
|
77
|
+
- **Probe URL mismatch (`/v1/v1` duplication)** between the continuous probe
|
|
78
|
+
and the adapters caused healthy backends to be wrongly demoted; URL
|
|
79
|
+
normalization is now shared
|
|
80
|
+
(`coderouter/adapters/anthropic_native.py`, `coderouter/guards/continuous_probe.py`, PR #34/H4).
|
|
81
|
+
- **`context_budget` dropped entire history at once** on threshold breach;
|
|
82
|
+
trimming is now sequential per tool-pair with re-estimation after each
|
|
83
|
+
step, and the new head turn is renormalized to a plain user turn
|
|
84
|
+
(`coderouter/guards/context_budget.py`, PR #34/H5).
|
|
85
|
+
- **Empty-stream termination violated the SSE protocol** by sending a
|
|
86
|
+
terminator with no preceding `message_start`; one is now synthesized
|
|
87
|
+
first (`coderouter/translation/convert.py`, PR #34/H6).
|
|
88
|
+
- **Excluded providers never recovered after a restart** — restored
|
|
89
|
+
self-healing state didn't re-arm recovery probes (only the live
|
|
90
|
+
UNHEALTHY-transition path did); probes are now re-spawned after state
|
|
91
|
+
restore, queued if the event loop isn't running yet
|
|
92
|
+
(`coderouter/routing/fallback.py`, `coderouter/guards/self_healing.py`, PR #34/H7).
|
|
93
|
+
- **Malformed upstream responses bypassed retry/fallback** — an uncaught
|
|
94
|
+
pydantic `ValidationError` on missing/misshaped fields now converts to a
|
|
95
|
+
retryable `AdapterError` on non-stream paths, and stream paths skip the
|
|
96
|
+
malformed chunk with a once-per-stream warning
|
|
97
|
+
(`coderouter/adapters/anthropic_native.py`, `coderouter/adapters/openai_compat.py`, PR #35/M6).
|
|
98
|
+
- **`"oom"` substring matched inside `"room"` / `"zoom"` / `"bloom"`** in
|
|
99
|
+
upstream error bodies, cooling down healthy providers; the memory-pressure
|
|
100
|
+
guard now matches the token on word boundaries
|
|
101
|
+
(`coderouter/guards/memory_pressure.py`, PR #35/M10).
|
|
102
|
+
- **Interleaved text/tool-call deltas sent `input_json_delta` to
|
|
103
|
+
already-closed block indices** — blocks are now reopened at a fresh index
|
|
104
|
+
with their original `id`/`name` preserved
|
|
105
|
+
(`coderouter/translation/convert.py`, PR #35/M7).
|
|
106
|
+
- **`tool_result.is_error` was dropped in both translation directions** — an
|
|
107
|
+
`"Error: "` content marker now round-trips the flag without
|
|
108
|
+
double-prefixing (`coderouter/translation/convert.py`, PR #35/M8).
|
|
109
|
+
- **Streams ending without `message_stop` sent no terminal chunk**, hanging
|
|
110
|
+
OpenAI-compatible clients; a `finish_reason` + usage chunk is now
|
|
111
|
+
synthesized on abnormal termination (mirrors the H6 fix in the other
|
|
112
|
+
direction) (`coderouter/translation/convert.py`, PR #35/M9).
|
|
113
|
+
- **`configure_logging` permanently detached the metrics collector** on a
|
|
114
|
+
second `create_app()` call by removing every root handler;
|
|
115
|
+
`install_collector` now re-attaches when missing, and
|
|
116
|
+
`configure_logging` removes only its own marked handlers
|
|
117
|
+
(`coderouter/logging.py`, PR #35/M4).
|
|
118
|
+
- **Metrics persistence save/load asymmetry** zeroed language-tax
|
|
119
|
+
accounting on every restart — `save_state` was missing
|
|
120
|
+
`language_tax_usd` / `language_tax_usd_aggregate`, which `load_state`
|
|
121
|
+
expects (`coderouter/metrics/collector.py`, PR #35/M5).
|
|
122
|
+
|
|
123
|
+
### Changed
|
|
124
|
+
|
|
125
|
+
- **Config validation now fails fast at load time.** Previously-silent
|
|
126
|
+
misconfiguration is now rejected: profile chains referencing unknown
|
|
127
|
+
provider names, duplicate provider/profile names, inverted thresholds
|
|
128
|
+
(e.g. `context_budget` warn > trim, `trim_target` ≥ `trim_threshold`,
|
|
129
|
+
`recovery_probe_initial_s` > `recovery_probe_max_s`), and
|
|
130
|
+
`has_tools: false` / `has_image: false` matchers (which could never
|
|
131
|
+
match) (`coderouter/config/schemas.py`, PR #35/M13).
|
|
132
|
+
- **Drift-based chain demotion now applies regardless of the `adaptive`
|
|
133
|
+
flag.** A dedicated cooldown-based demotion map is applied in
|
|
134
|
+
`_resolve_anthropic_chain` unconditionally; previously demotion only
|
|
135
|
+
affected ordering when `profile.adaptive` was true (and did nothing
|
|
136
|
+
under five samples) (`coderouter/routing/fallback.py`, PR #35/M3).
|
|
137
|
+
- **Adaptive routing now observes streaming paths.** `record_attempt` was
|
|
138
|
+
only called from `generate_anthropic`; all four entry points
|
|
139
|
+
(`stream_anthropic` / `stream` / `generate` and non-stream) now record
|
|
140
|
+
first-event latency and outcomes, including Claude Code's default
|
|
141
|
+
streaming path (`coderouter/routing/fallback.py`, PR #35/M2).
|
|
142
|
+
- **Drift verdicts are now request-scoped (`ContextVar`)** instead of
|
|
143
|
+
stored on the shared engine, eliminating a race where concurrent
|
|
144
|
+
requests could read another request's drift header or a stale verdict
|
|
145
|
+
during cooldown; the engine attribute remains as a deprecated mirror
|
|
146
|
+
(`coderouter/routing/fallback.py`, PR #35/M1).
|
|
147
|
+
- **Chain resolution and context estimation no longer run twice per
|
|
148
|
+
request** — the resolved dispatch is cached per request and reused when
|
|
149
|
+
the request object is unchanged (`coderouter/routing/fallback.py`, PR #35/M11).
|
|
150
|
+
- **Request/audit logs are now buffered** (20 records or 2 seconds,
|
|
151
|
+
flushed on close/`atexit`; `flush_every_n=1` restores write-through)
|
|
152
|
+
instead of synchronous per-request writes
|
|
153
|
+
(`coderouter/state/audit_log.py`, `coderouter/state/request_log.py`, PR #35/M12).
|
|
154
|
+
- **The metrics collector hot path exits early** for unrecognized events
|
|
155
|
+
before acquiring the lock, and known events are dispatched via a dict
|
|
156
|
+
table instead of a ~30-branch `if`/`elif` chain
|
|
157
|
+
(`coderouter/metrics/collector.py`, PR #35/M12).
|
|
158
|
+
- **Adapters now share a single `httpx.AsyncClient`** (lazily created,
|
|
159
|
+
per-call timeout override, closed on shutdown) instead of creating one
|
|
160
|
+
per request, enabling keep-alive/TLS session reuse
|
|
161
|
+
(`coderouter/adapters/base.py`, `coderouter/adapters/openai_compat.py`,
|
|
162
|
+
`coderouter/adapters/anthropic_native.py`, `coderouter/ingress/app.py`, PR #34/H3).
|
|
163
|
+
|
|
164
|
+
### Security
|
|
165
|
+
|
|
166
|
+
- **Host-validation middleware** on all routes rejects non-loopback `Host`
|
|
167
|
+
headers as a DNS-rebinding defense; loopback
|
|
168
|
+
(`localhost` / `127.0.0.1` / `[::1]`) is allowed by default and
|
|
169
|
+
`CODEROUTER_ALLOWED_HOSTS` (comma-separated) extends the allowlist
|
|
170
|
+
(`coderouter/ingress/app.py`, PR #34/H8).
|
|
171
|
+
- **Opt-in launcher token authentication.** `launcher start/stop/delete`
|
|
172
|
+
require the `X-CodeRouter-Token` header (constant-time compare) when
|
|
173
|
+
`CODEROUTER_LAUNCHER_TOKEN` is set; unset behaves exactly as before, with
|
|
174
|
+
a one-time startup warning (`coderouter/ingress/launcher_routes.py`, PR #34/H8).
|
|
175
|
+
- **`-m`/`--model` re-specification rejected** in launcher `extra_args`/
|
|
176
|
+
`options` to block arbitrary model-path injection
|
|
177
|
+
(`coderouter/ingress/launcher_routes.py`, PR #34/H8).
|
|
178
|
+
- **Request body size cap** (64 MB default, `CODEROUTER_MAX_BODY_BYTES`
|
|
179
|
+
override) returns 413; SSE responses are unaffected
|
|
180
|
+
(`coderouter/ingress/app.py`, PR #35/M14).
|
|
181
|
+
- **`/api/launcher/suggest` path confinement** — resolved paths must now
|
|
182
|
+
fall inside the configured `model_dirs`, closing an arbitrary-path
|
|
183
|
+
existence/size probe (`coderouter/ingress/launcher_routes.py`, PR #35/M14).
|
|
184
|
+
|
|
185
|
+
### Tests
|
|
186
|
+
|
|
187
|
+
- Full suite grew from **1263 → 1401 passed** (1 skipped, environment-only)
|
|
188
|
+
across the two PRs: 139 new regression tests in 12 new test files
|
|
189
|
+
(`tests/test_fix_h*.py` ×6, `tests/test_fix_m*.py` ×6). One existing test,
|
|
190
|
+
`tests/test_auto_router.py::test_has_tools_false_rejected_at_load`, was
|
|
191
|
+
flipped to expect `ValidationError` — its own docstring had anticipated
|
|
192
|
+
this once the new config validator (M13) landed; it is the only existing
|
|
193
|
+
test changed by either PR.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
9
197
|
## [v2.6.1] — 2026-06-28 (Token-savings accounting)
|
|
10
198
|
|
|
11
199
|
Patch release: surfaces **token-savings accounting** in the metrics layer
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.
|
|
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
|
|
|
@@ -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."""
|