coderouter-cli 2.7.2__tar.gz → 2.7.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/CHANGELOG.md +83 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/PKG-INFO +3 -1
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/README.en.md +2 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/README.md +2 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/config/schemas.py +51 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/launcher_routes.py +51 -1
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/openai_routes.py +85 -8
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/logging.py +40 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/metrics/collector.py +29 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/routing/fallback.py +290 -5
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/launcher.md +25 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/pyproject.toml +1 -1
- coderouter_cli-2.7.4/tests/test_fallback_empty_response.py +561 -0
- coderouter_cli-2.7.4/tests/test_launcher_provider_sync.py +124 -0
- coderouter_cli-2.7.4/tests/test_models_passthrough.py +144 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/.gitignore +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/LICENSE +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/adapters/anthropic_native.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/adapters/base.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/adapters/openai_compat.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/cli.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/config/loader.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/cost.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/errors.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/context_budget.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/self_healing.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/anthropic_routes.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/routing/capability.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/state/audit_log.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/state/request_log.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/translation/convert.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/README.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/llamacpp-direct.en.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/llamacpp-direct.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/concepts/context-budget.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/security.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/.env.example +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/README.md +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers-multiagent.yaml +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.llamacpp-vllm.yaml +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.raspberrypi.yaml +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/examples/providers.yaml +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/__init__.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/conftest.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_auto_router.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_budget.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_capability.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_capability_tool_choice.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_cli.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_config.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_config_shim_actions.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_context_budget.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_errors.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_cache_strip.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fallback_tool_choice_emulate.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_h1_prometheus_drift.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_h2_h6_translation.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_h3_client_reuse.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_h4_h5_guards.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_h7_probe_restore.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_h8_launcher_auth.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_mA_routing.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_mB_adapters.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_mC_convert.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_mD_logging_metrics.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_mE_config_validation.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_fix_mF_ingress.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_ingress_count_tokens.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_tool_repair_lenient.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_tool_repair_r4.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_toolrepair_bench.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.7.2 → coderouter_cli-2.7.4}/tests/test_translation_reverse.py +0 -0
|
@@ -6,6 +6,89 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [v2.7.4] — 2026-07-06 (Launcher provider auto-sync + /v1/models passthrough)
|
|
10
|
+
|
|
11
|
+
Two PRs closing the "launcher started it, but nothing can route to it /
|
|
12
|
+
nothing can *name* it" gap, driven by a real field failure (launcher on
|
|
13
|
+
port 8085, providers.yaml pointing at 8080; external benchmark reports
|
|
14
|
+
indistinguishable across loaded GGUFs). **PR #48** (models passthrough)
|
|
15
|
+
and **PR #49** (launcher auto-sync). Fully backward compatible, no new
|
|
16
|
+
dependencies. Verified end-to-end on real hardware (NucBox EVO-X2,
|
|
17
|
+
llama.cpp + omnicoder-9b): zero-config launch → route via
|
|
18
|
+
`X-CodeRouter-Profile: launcher` → response carries the real GGUF name.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **`/v1/models` upstream passthrough** for empty-model providers.
|
|
23
|
+
A provider with `model: ""` (the launcher/llama-server pattern — the
|
|
24
|
+
upstream decides what is loaded) used to be listed by its static
|
|
25
|
+
provider name only. Now `/v1/models` queries the upstream's own
|
|
26
|
+
`/models` (2s timeout, 30s TTL cache) and surfaces the real loaded
|
|
27
|
+
model id(s) as `owned_by: coderouter/<provider>`. Any failure falls
|
|
28
|
+
back to the historic provider-name entry; providers with a configured
|
|
29
|
+
model keep the exact previous shape. External benchmarks (e.g.
|
|
30
|
+
SWE-bench harnesses with `model: auto`) can now tell GGUF swaps apart
|
|
31
|
+
without config edits. (PR #48)
|
|
32
|
+
- **Launcher provider auto-sync.** A backend started from the embedded
|
|
33
|
+
launcher (`/api/launcher/start`) is auto-registered as a routable
|
|
34
|
+
provider named `launcher-<backend>-<port>` (restart on the same port
|
|
35
|
+
replaces, never duplicates) and placed at the FRONT of an on-demand
|
|
36
|
+
`launcher` profile. `default_profile` is never touched — routing is an
|
|
37
|
+
explicit opt-in via `X-CodeRouter-Profile: launcher` or body
|
|
38
|
+
`profile`. Registration is deliberately **in-memory only** (a YAML
|
|
39
|
+
rewrite would destroy operator comments; provider and process share
|
|
40
|
+
the server's lifetime). Sync failures never fail the start; the
|
|
41
|
+
result rides on the start response as `provider_sync`. New engine
|
|
42
|
+
API: `FallbackEngine.register_provider()`. (PR #49)
|
|
43
|
+
|
|
44
|
+
### Docs
|
|
45
|
+
|
|
46
|
+
- README (JA/EN) launcher feature table + `docs/backends/launcher.md`:
|
|
47
|
+
auto-sync semantics, verification curl recipes, and the desktop-GUI
|
|
48
|
+
caveat (launcher_gui.py runs out-of-process and is not synced).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## [v2.7.3] — 2026-07-05 (Per-request empty-response fallback + L3 bench evidence)
|
|
53
|
+
|
|
54
|
+
Two PRs: **PR #45** (`empty_response_action` — the fallback leg of the
|
|
55
|
+
three-tier tool-call reliability story) and **PR #46** (L3 benchmark
|
|
56
|
+
refresh: model matrix, repairer-version A/B archives, results). Fully
|
|
57
|
+
backward compatible: new knob defaults to `off`, no breaking config
|
|
58
|
+
changes, no new dependencies.
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- **`FallbackChain.empty_response_action: off | warn | fallback`**
|
|
63
|
+
(default `off`). With `fallback`, a 200 response that is *content-empty*
|
|
64
|
+
(no `tool_use`, no non-whitespace text; thinking-only counts as empty) is
|
|
65
|
+
re-dispatched in-flight to the next provider in the chain. Non-streaming
|
|
66
|
+
responses are judged on the finalized object; streaming buffers events
|
|
67
|
+
until the first real content token and discards an unobserved stream.
|
|
68
|
+
Emptiness is judged on content, not `usage.output_tokens` (unreliable on
|
|
69
|
+
some backends). Motivation: gemma4:26b returns blank 200s on
|
|
70
|
+
`no_tool_temptation`-type prompts (20/20 at temperature 0, direct AND via
|
|
71
|
+
router) — no text exists for the repair layer to fix, and the windowed
|
|
72
|
+
drift guard `empty_response_rate` cannot rescue a single blank turn
|
|
73
|
+
in-flight. 21 new tests. (PR #45)
|
|
74
|
+
- **Benchmark refresh**: full L3 model matrix in
|
|
75
|
+
`benchmarks/tool-repair/providers.bench.yaml` (llama3.2/3.1, mistral,
|
|
76
|
+
qwen2.5-coder:1.5b, phi4-mini + originals, `bench-gemma-chain` enabled),
|
|
77
|
+
one-shot matrix runner `bench_l3_p1.sh`, latest live results plus
|
|
78
|
+
repairer-version A/B snapshots under `results/archive-v2.7.{0,1}/`.
|
|
79
|
+
(PR #46)
|
|
80
|
+
|
|
81
|
+
### Results
|
|
82
|
+
|
|
83
|
+
- Live chain validation (M3 Max, temperature 0, 100 requests, zero
|
|
84
|
+
errors): gemma4:26b → qwen3-coder:30b chain **80% → 100%** native —
|
|
85
|
+
all 20 blank turns rescued.
|
|
86
|
+
- Three-tier story now fully measured: repair rescues broken-but-present
|
|
87
|
+
calls (qwen2.5×2 / mistral 0→100, phi4 +60pt), healthy models pass
|
|
88
|
+
through undegraded, and fallback closes what repair cannot touch.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
9
92
|
## [v2.7.2] — 2026-07-05 (R4 tool-call repair: nested-XML / JSON-envelope / call-syntax forms)
|
|
10
93
|
|
|
11
94
|
Single feature PR **#43**, driven by the 2026-07-05 L3 live benchmark
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.4
|
|
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
|
|
@@ -220,6 +220,8 @@ providers:
|
|
|
220
220
|
| **オプションプロファイル** | `providers.yaml` に名前付きプリセットを定義 → ドロップダウンで選択するだけ |
|
|
221
221
|
| **複数プロセス管理** | llama.cpp と vllm を同時に起動し、ポートごとに独立管理 |
|
|
222
222
|
| **ログビューア** | 各プロセスの stdout/stderr をブラウザ内でリアルタイム確認 |
|
|
223
|
+
| **provider 自動同期** (v2.7.4) | 起動したバックエンドを provider として自動登録(`launcher-llamacpp-8085` 等)。providers.yaml 無編集で `X-CodeRouter-Profile: launcher` からルーティング可能。メモリ内のみ・serve と同寿命 |
|
|
224
|
+
| **モデル名パススルー** (v2.7.4) | `model: ""` の provider は `/v1/models` が上流のロード中モデル ID(gguf 名)をそのまま返す。gguf を差し替えても config 編集不要 — 外部ベンチからモデルを識別できる |
|
|
223
225
|
|
|
224
226
|
```yaml
|
|
225
227
|
# providers.yaml に追記するだけで有効になる
|
|
@@ -156,6 +156,8 @@ Browser UI at `http://localhost:8088/launcher` for starting and managing local i
|
|
|
156
156
|
| **Option profiles** | Name your flag presets in `providers.yaml` — select from a dropdown, no CLI needed |
|
|
157
157
|
| **Multi-process** | Run llama.cpp and vllm side by side on different ports |
|
|
158
158
|
| **Log viewer** | stdout/stderr of each process shown live in the browser |
|
|
159
|
+
| **Provider auto-sync** (v2.7.4) | A started backend is auto-registered as a routable provider (`launcher-llamacpp-8085`); route to it with `X-CodeRouter-Profile: launcher` — zero providers.yaml edits. In-memory, shares the server's lifetime |
|
|
160
|
+
| **Model-id passthrough** (v2.7.4) | Providers with `model: ""` make `/v1/models` surface the upstream's actually-loaded model id (the GGUF name). Swap models without touching config — external benchmarks can tell runs apart |
|
|
159
161
|
|
|
160
162
|
```yaml
|
|
161
163
|
# Add to providers.yaml — no code changes needed
|
|
@@ -175,6 +175,8 @@ providers:
|
|
|
175
175
|
| **オプションプロファイル** | `providers.yaml` に名前付きプリセットを定義 → ドロップダウンで選択するだけ |
|
|
176
176
|
| **複数プロセス管理** | llama.cpp と vllm を同時に起動し、ポートごとに独立管理 |
|
|
177
177
|
| **ログビューア** | 各プロセスの stdout/stderr をブラウザ内でリアルタイム確認 |
|
|
178
|
+
| **provider 自動同期** (v2.7.4) | 起動したバックエンドを provider として自動登録(`launcher-llamacpp-8085` 等)。providers.yaml 無編集で `X-CodeRouter-Profile: launcher` からルーティング可能。メモリ内のみ・serve と同寿命 |
|
|
179
|
+
| **モデル名パススルー** (v2.7.4) | `model: ""` の provider は `/v1/models` が上流のロード中モデル ID(gguf 名)をそのまま返す。gguf を差し替えても config 編集不要 — 外部ベンチからモデルを識別できる |
|
|
178
180
|
|
|
179
181
|
```yaml
|
|
180
182
|
# providers.yaml に追記するだけで有効になる
|
|
@@ -718,6 +718,57 @@ class FallbackChain(BaseModel):
|
|
|
718
718
|
),
|
|
719
719
|
)
|
|
720
720
|
|
|
721
|
+
# --- ⑧ (empty-response): per-request empty-response fallback ------------
|
|
722
|
+
#
|
|
723
|
+
# Some local backends (observed: gemma4:26b on ``no_tool_temptation``
|
|
724
|
+
# prompts) return a 200 with a structurally-valid but *content-empty*
|
|
725
|
+
# Anthropic response — no tool_use and no non-whitespace text — for a
|
|
726
|
+
# fraction of requests. The drift guard's ``empty_response_rate`` is a
|
|
727
|
+
# windowed aggregate (default threshold 0.3, ``min_window_fill`` 6): it
|
|
728
|
+
# promotes/reloads a backend once the *rate* is bad, but cannot rescue a
|
|
729
|
+
# single blank turn in-flight. This knob adds a per-request in-flight
|
|
730
|
+
# fallback that re-dispatches the *same* request to the next provider in
|
|
731
|
+
# the chain the moment an empty response is detected.
|
|
732
|
+
#
|
|
733
|
+
# Design: "empty" is judged on *content*, not usage.output_tokens (which
|
|
734
|
+
# some backends report unreliably). A response is empty when its content
|
|
735
|
+
# list is empty, or every block is either a whitespace-only ``text`` block
|
|
736
|
+
# or a ``thinking`` block — i.e. nothing the client can act on. A single
|
|
737
|
+
# ``tool_use`` block or one non-whitespace ``text`` block makes it non-empty.
|
|
738
|
+
#
|
|
739
|
+
# * ``off`` — no detection, no fallback, no log. Backward-compatible
|
|
740
|
+
# default (byte-for-byte identical to pre-⑧ behavior).
|
|
741
|
+
# * ``warn`` — detect + emit an ``empty-response-detected`` log line
|
|
742
|
+
# only; the empty response is returned unchanged.
|
|
743
|
+
# * ``fallback`` — on an empty response, log ``empty-response-detected``
|
|
744
|
+
# and continue to the next provider (the empty response
|
|
745
|
+
# is *not* recorded as an error). If every provider in
|
|
746
|
+
# the chain returns empty, the last empty response is
|
|
747
|
+
# returned as-is (a 200 blank is a legitimate answer)
|
|
748
|
+
# with ``chain_exhausted=True`` on the log line. On the
|
|
749
|
+
# streaming path, ``fallback`` buffers events until real
|
|
750
|
+
# content is observed, so an empty stream can be swapped
|
|
751
|
+
# to the next provider without the client seeing bytes.
|
|
752
|
+
#
|
|
753
|
+
# Default ``off`` preserves complete backward compatibility; the original
|
|
754
|
+
# request object is never mutated, so a later provider always receives the
|
|
755
|
+
# untouched request.
|
|
756
|
+
empty_response_action: Literal["off", "warn", "fallback"] = Field(
|
|
757
|
+
default="off",
|
|
758
|
+
description=(
|
|
759
|
+
"⑧ (empty-response): action when a provider returns a 200 with "
|
|
760
|
+
"empty content (no tool_use and no non-whitespace text; a "
|
|
761
|
+
"thinking-only response counts as empty). ``off`` (default) does "
|
|
762
|
+
"nothing. ``warn`` emits an ``empty-response-detected`` log only "
|
|
763
|
+
"and returns the empty response. ``fallback`` re-dispatches the "
|
|
764
|
+
"same request to the next provider (empty is not counted as an "
|
|
765
|
+
"error); if the whole chain returns empty, the last empty response "
|
|
766
|
+
"is returned unchanged. Streaming buffers events until real "
|
|
767
|
+
"content appears so empty streams can be swapped provider-side "
|
|
768
|
+
"without the client seeing any bytes."
|
|
769
|
+
),
|
|
770
|
+
)
|
|
771
|
+
|
|
721
772
|
# --- S2 (shim): tool_choice capability gate + emulation -----------------
|
|
722
773
|
#
|
|
723
774
|
# Anthropic clients (Claude Code, SDKs) can pin the model to a specific
|
|
@@ -655,6 +655,28 @@ async def api_backends(request: Request) -> dict[str, Any]:
|
|
|
655
655
|
return {"backends": result}
|
|
656
656
|
|
|
657
657
|
|
|
658
|
+
def _launcher_provider_config(backend: str, port: int) -> Any:
|
|
659
|
+
"""Build the provider entry for a launcher-started backend.
|
|
660
|
+
|
|
661
|
+
``model`` is left EMPTY on purpose: llama-server / vllm / mlx decide
|
|
662
|
+
which model is actually loaded, and the empty-model /v1/models
|
|
663
|
+
passthrough then surfaces the upstream's real model id to benchmark
|
|
664
|
+
clients — swap the GGUF, no config edit needed.
|
|
665
|
+
|
|
666
|
+
Name embeds backend + port (``launcher-llamacpp-8085``) so restarting
|
|
667
|
+
on the same port REPLACES the entry instead of piling up duplicates.
|
|
668
|
+
"""
|
|
669
|
+
from coderouter.config.schemas import ProviderConfig
|
|
670
|
+
|
|
671
|
+
safe_backend = backend.replace(".", "")
|
|
672
|
+
return ProviderConfig(
|
|
673
|
+
name=f"launcher-{safe_backend}-{port}",
|
|
674
|
+
base_url=f"http://localhost:{port}/v1",
|
|
675
|
+
model="",
|
|
676
|
+
timeout_s=120.0,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
|
|
658
680
|
@router.post("/api/launcher/start")
|
|
659
681
|
async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
|
|
660
682
|
"""Start a new backend process."""
|
|
@@ -717,7 +739,35 @@ async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
|
|
|
717
739
|
_background_tasks.add(_task)
|
|
718
740
|
_task.add_done_callback(_background_tasks.discard)
|
|
719
741
|
|
|
720
|
-
|
|
742
|
+
# Provider auto-sync: register the just-started backend as a routable
|
|
743
|
+
# provider so requests can reach it without hand-editing providers.yaml
|
|
744
|
+
# (in-memory only — see FallbackEngine.register_provider docstring).
|
|
745
|
+
# Sync failure must never fail the start itself.
|
|
746
|
+
provider_sync: dict[str, Any] | None = None
|
|
747
|
+
engine = getattr(request.app.state, "engine", None)
|
|
748
|
+
if engine is not None and hasattr(engine, "register_provider"):
|
|
749
|
+
try:
|
|
750
|
+
provider_sync = engine.register_provider(
|
|
751
|
+
_launcher_provider_config(req.backend, req.port)
|
|
752
|
+
)
|
|
753
|
+
proc.log_tail.append(
|
|
754
|
+
"[launcher] provider sync: "
|
|
755
|
+
f"{provider_sync['provider']} -> profile "
|
|
756
|
+
f"'{provider_sync['profile']}' (in-memory)"
|
|
757
|
+
)
|
|
758
|
+
except Exception as exc:
|
|
759
|
+
logger.warning(
|
|
760
|
+
"launcher provider sync failed",
|
|
761
|
+
extra={"backend": req.backend, "port": req.port, "error": str(exc)},
|
|
762
|
+
)
|
|
763
|
+
proc.log_tail.append(f"[launcher] provider sync failed: {exc}")
|
|
764
|
+
|
|
765
|
+
return {
|
|
766
|
+
"id": proc_id,
|
|
767
|
+
"pid": p.pid,
|
|
768
|
+
"command": cmd,
|
|
769
|
+
"provider_sync": provider_sync,
|
|
770
|
+
}
|
|
721
771
|
|
|
722
772
|
|
|
723
773
|
@router.post("/api/launcher/stop/{proc_id}")
|
|
@@ -78,22 +78,99 @@ def _resolve_stream_timeout_s(engine: FallbackEngine, profile: str | None) -> fl
|
|
|
78
78
|
return max(_STREAM_TIMEOUT_MIN_S, float(per_call) * _STREAM_TIMEOUT_MULTIPLIER)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
# --- /v1/models upstream passthrough -------------------------------------
|
|
82
|
+
#
|
|
83
|
+
# Providers with an *empty* ``model`` field are passthrough providers: the
|
|
84
|
+
# upstream (llama-server, LM Studio, ...) decides which model is loaded and
|
|
85
|
+
# CodeRouter never sends a model name. For those providers the historic
|
|
86
|
+
# behaviour — returning the provider *name* — makes external benchmarks
|
|
87
|
+
# indistinguishable across loaded GGUFs (the launcher_gui setup always
|
|
88
|
+
# reported ``llama-cpp-local`` regardless of the selected model). We now ask
|
|
89
|
+
# the upstream's own ``/models`` endpoint for its real model id(s) and
|
|
90
|
+
# surface those instead.
|
|
91
|
+
#
|
|
92
|
+
# Guards: only for ``kind == "openai_compat"`` with ``model == ""``; a short
|
|
93
|
+
# per-upstream TTL cache keeps repeated SDK probes cheap; ANY failure (refused
|
|
94
|
+
# connection, timeout, unexpected shape) falls back to the historic
|
|
95
|
+
# provider-name entry, so this is strictly additive. Providers with a
|
|
96
|
+
# configured model keep the exact previous behaviour.
|
|
97
|
+
|
|
98
|
+
_UPSTREAM_MODELS_TTL_S = 30.0
|
|
99
|
+
_UPSTREAM_MODELS_TIMEOUT_S = 2.0
|
|
100
|
+
# base_url -> (expires_monotonic, ids)
|
|
101
|
+
_upstream_models_cache: dict[str, tuple[float, list[str]]] = {}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def _fetch_upstream_model_ids(base_url: str) -> list[str]:
|
|
105
|
+
"""GET {base_url}/models and return the upstream model ids.
|
|
106
|
+
|
|
107
|
+
Raises on any transport / shape problem — the caller treats every
|
|
108
|
+
exception as "fall back to the provider-name entry".
|
|
109
|
+
"""
|
|
110
|
+
import httpx # local import keeps module import time flat
|
|
111
|
+
|
|
112
|
+
url = base_url.rstrip("/") + "/models"
|
|
113
|
+
async with httpx.AsyncClient(timeout=_UPSTREAM_MODELS_TIMEOUT_S) as client:
|
|
114
|
+
resp = await client.get(url)
|
|
115
|
+
resp.raise_for_status()
|
|
116
|
+
payload = resp.json()
|
|
117
|
+
ids = [
|
|
118
|
+
str(item["id"])
|
|
119
|
+
for item in payload.get("data", [])
|
|
120
|
+
if isinstance(item, dict) and item.get("id")
|
|
121
|
+
]
|
|
122
|
+
if not ids:
|
|
123
|
+
raise ValueError(f"upstream {url} returned no model ids")
|
|
124
|
+
return ids
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def _upstream_model_ids_cached(base_url: str) -> list[str]:
|
|
128
|
+
"""TTL-cached wrapper around :func:`_fetch_upstream_model_ids`."""
|
|
129
|
+
now = time.monotonic()
|
|
130
|
+
hit = _upstream_models_cache.get(base_url)
|
|
131
|
+
if hit is not None and hit[0] > now:
|
|
132
|
+
return hit[1]
|
|
133
|
+
ids = await _fetch_upstream_model_ids(base_url)
|
|
134
|
+
_upstream_models_cache[base_url] = (now + _UPSTREAM_MODELS_TTL_S, ids)
|
|
135
|
+
return ids
|
|
136
|
+
|
|
137
|
+
|
|
81
138
|
@router.get("/models")
|
|
82
139
|
async def list_models(request: Request) -> dict[str, object]:
|
|
83
|
-
"""
|
|
140
|
+
"""/v1/models: provider names, with upstream passthrough for empty-model
|
|
141
|
+
providers (see the passthrough note above)."""
|
|
84
142
|
config = request.app.state.config
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
143
|
+
created = int(time.time())
|
|
144
|
+
data: list[dict[str, object]] = []
|
|
145
|
+
for p in config.providers:
|
|
146
|
+
if p.model == "" and p.kind == "openai_compat":
|
|
147
|
+
try:
|
|
148
|
+
ids = await _upstream_model_ids_cached(str(p.base_url))
|
|
149
|
+
except Exception: # any failure means "no passthrough"
|
|
150
|
+
logger.debug(
|
|
151
|
+
"models passthrough failed; falling back to provider name",
|
|
152
|
+
extra={"provider": p.name},
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
data.extend(
|
|
156
|
+
{
|
|
157
|
+
"id": model_id,
|
|
158
|
+
"object": "model",
|
|
159
|
+
"created": created,
|
|
160
|
+
"owned_by": f"coderouter/{p.name}",
|
|
161
|
+
}
|
|
162
|
+
for model_id in ids
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
data.append(
|
|
88
166
|
{
|
|
89
167
|
"id": p.name,
|
|
90
168
|
"object": "model",
|
|
91
|
-
"created":
|
|
169
|
+
"created": created,
|
|
92
170
|
"owned_by": "coderouter",
|
|
93
171
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
172
|
+
)
|
|
173
|
+
return {"object": "list", "data": data}
|
|
97
174
|
|
|
98
175
|
|
|
99
176
|
@router.post("/chat/completions", response_model=None)
|
|
@@ -1372,6 +1372,46 @@ def log_partial_stitch_surfaced(
|
|
|
1372
1372
|
)
|
|
1373
1373
|
|
|
1374
1374
|
|
|
1375
|
+
# ---------------------------------------------------------------------------
|
|
1376
|
+
# ⑧ (empty-response): per-request empty-response fallback log shape
|
|
1377
|
+
#
|
|
1378
|
+
# Fired when a provider returns a 200 with content-empty output (no
|
|
1379
|
+
# tool_use and no non-whitespace text). The single event lane carries the
|
|
1380
|
+
# ``action`` (warn|fallback) that triggered it, whether the empty response
|
|
1381
|
+
# came from the streaming or non-streaming path, and ``chain_exhausted``
|
|
1382
|
+
# — True only when every provider in the chain returned empty and the last
|
|
1383
|
+
# empty response is being returned to the client as-is.
|
|
1384
|
+
# ---------------------------------------------------------------------------
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
def log_empty_response_detected(
|
|
1388
|
+
logger: logging.Logger,
|
|
1389
|
+
provider: str,
|
|
1390
|
+
*,
|
|
1391
|
+
action: str,
|
|
1392
|
+
stream: bool,
|
|
1393
|
+
chain_exhausted: bool = False,
|
|
1394
|
+
) -> None:
|
|
1395
|
+
"""Emit an ``empty-response-detected`` warn line.
|
|
1396
|
+
|
|
1397
|
+
Fired when ``empty_response_action`` is ``warn`` or ``fallback`` and a
|
|
1398
|
+
provider returns a structurally-valid but content-empty response. The
|
|
1399
|
+
``action`` field records which knob value produced the line; ``stream``
|
|
1400
|
+
distinguishes the SSE path from the non-streaming path; ``chain_exhausted``
|
|
1401
|
+
is True only when the whole chain returned empty and the last empty
|
|
1402
|
+
response is being returned unchanged.
|
|
1403
|
+
"""
|
|
1404
|
+
logger.warning(
|
|
1405
|
+
"empty-response-detected",
|
|
1406
|
+
extra={
|
|
1407
|
+
"provider": provider,
|
|
1408
|
+
"action": action,
|
|
1409
|
+
"stream": stream,
|
|
1410
|
+
"chain_exhausted": chain_exhausted,
|
|
1411
|
+
},
|
|
1412
|
+
)
|
|
1413
|
+
|
|
1414
|
+
|
|
1375
1415
|
# ---------------------------------------------------------------------------
|
|
1376
1416
|
# v2.0-I: Continuous probe log shapes
|
|
1377
1417
|
#
|
|
@@ -52,6 +52,7 @@ Event inventory (dispatch table in :meth:`MetricsCollector._dispatch`)
|
|
|
52
52
|
``drift-promoted`` (v2.0-G) → ``drift_promoted_total``
|
|
53
53
|
``drift-reload-attempted`` → ``drift_reload_total`` / success
|
|
54
54
|
``partial-stitch-surfaced`` → ``partial_stitch_surfaced_total`` (v2.0-H)
|
|
55
|
+
``empty-response-detected`` → ``empty_responses_total`` + per-provider (⑧)
|
|
55
56
|
``probe-completed`` (v2.0-I) → ``probe_total`` / ``probe_success`` / ``probe_failure``
|
|
56
57
|
+ per-provider latency gauge
|
|
57
58
|
``probe-round-completed`` → ``probe_rounds_total`` (v2.0-I)
|
|
@@ -109,6 +110,7 @@ _KNOWN_EVENTS: Final[frozenset[str]] = frozenset(
|
|
|
109
110
|
"drift-promoted",
|
|
110
111
|
"drift-reload-attempted",
|
|
111
112
|
"partial-stitch-surfaced",
|
|
113
|
+
"empty-response-detected",
|
|
112
114
|
"probe-completed",
|
|
113
115
|
"probe-round-completed",
|
|
114
116
|
"probe-capabilities-drift",
|
|
@@ -272,6 +274,13 @@ class MetricsCollector(logging.Handler):
|
|
|
272
274
|
# client instead of a generic error event.
|
|
273
275
|
self._partial_stitch_surfaced_total: int = 0
|
|
274
276
|
|
|
277
|
+
# ⑧ (empty-response): count of per-request empty-response detections
|
|
278
|
+
# (both ``warn`` and ``fallback`` actions). Aggregate + per-provider
|
|
279
|
+
# so a dashboard can spot which backend blanks out. Counts every
|
|
280
|
+
# detection event, including the terminal chain-exhausted one.
|
|
281
|
+
self._empty_responses_total: int = 0
|
|
282
|
+
self._empty_responses_by_provider: Counter[str] = Counter()
|
|
283
|
+
|
|
275
284
|
# v2.0-I: continuous probe counters. Per-provider probe attempts
|
|
276
285
|
# and outcomes, plus a round counter for the dashboard's
|
|
277
286
|
# "probes/min" panel.
|
|
@@ -329,6 +338,7 @@ class MetricsCollector(logging.Handler):
|
|
|
329
338
|
"drift-promoted": self._on_drift_promoted,
|
|
330
339
|
"drift-reload-attempted": self._on_drift_reload_attempted,
|
|
331
340
|
"partial-stitch-surfaced": self._on_partial_stitch_surfaced,
|
|
341
|
+
"empty-response-detected": self._on_empty_response_detected,
|
|
332
342
|
"probe-completed": self._on_probe_completed,
|
|
333
343
|
"probe-round-completed": self._on_probe_round_completed,
|
|
334
344
|
"probe-capabilities-drift": self._on_probe_capabilities_drift,
|
|
@@ -670,6 +680,17 @@ class MetricsCollector(logging.Handler):
|
|
|
670
680
|
self._partial_stitch_surfaced_total += 1
|
|
671
681
|
self._push_recent("partial-stitch-surfaced", extras, record)
|
|
672
682
|
|
|
683
|
+
def _on_empty_response_detected(
|
|
684
|
+
self, extras: dict[str, Any], record: logging.LogRecord
|
|
685
|
+
) -> None:
|
|
686
|
+
# ⑧ (empty-response): a content-empty 200 was detected under a
|
|
687
|
+
# ``warn`` / ``fallback`` action.
|
|
688
|
+
self._empty_responses_total += 1
|
|
689
|
+
provider = _str(extras.get("provider"))
|
|
690
|
+
if provider:
|
|
691
|
+
self._empty_responses_by_provider[provider] += 1
|
|
692
|
+
self._push_recent("empty-response-detected", extras, record)
|
|
693
|
+
|
|
673
694
|
def _on_probe_completed(
|
|
674
695
|
self, extras: dict[str, Any], record: logging.LogRecord
|
|
675
696
|
) -> None:
|
|
@@ -888,6 +909,11 @@ class MetricsCollector(logging.Handler):
|
|
|
888
909
|
"drift_reload_success_total": self._drift_reload_success_total,
|
|
889
910
|
# v2.0-H (L6): partial stitch surfaced.
|
|
890
911
|
"partial_stitch_surfaced_total": self._partial_stitch_surfaced_total,
|
|
912
|
+
# ⑧ (empty-response): per-request empty-response fallback.
|
|
913
|
+
"empty_responses_total": self._empty_responses_total,
|
|
914
|
+
"empty_responses_by_provider": dict(
|
|
915
|
+
self._empty_responses_by_provider
|
|
916
|
+
),
|
|
891
917
|
# v2.0-I: continuous probe counters.
|
|
892
918
|
"probe_total": dict(self._probe_total),
|
|
893
919
|
"probe_success": dict(self._probe_success),
|
|
@@ -1042,6 +1068,9 @@ class MetricsCollector(logging.Handler):
|
|
|
1042
1068
|
self._tokens_saved_by_mechanism.clear()
|
|
1043
1069
|
# v2.0-H (L6)
|
|
1044
1070
|
self._partial_stitch_surfaced_total = 0
|
|
1071
|
+
# ⑧ (empty-response)
|
|
1072
|
+
self._empty_responses_total = 0
|
|
1073
|
+
self._empty_responses_by_provider.clear()
|
|
1045
1074
|
# v2.0-I
|
|
1046
1075
|
self._probe_total.clear()
|
|
1047
1076
|
self._probe_success.clear()
|