coderouter-cli 2.2.0__tar.gz → 2.3.0a3__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.
Files changed (182) hide show
  1. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/CHANGELOG.md +139 -0
  2. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/PKG-INFO +1 -1
  3. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/config/schemas.py +61 -1
  4. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/ingress/app.py +8 -1
  5. coderouter_cli-2.3.0a3/coderouter/plugins/__init__.py +59 -0
  6. coderouter_cli-2.3.0a3/coderouter/plugins/base.py +168 -0
  7. coderouter_cli-2.3.0a3/coderouter/plugins/loader.py +176 -0
  8. coderouter_cli-2.3.0a3/coderouter/plugins/registry.py +83 -0
  9. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/routing/fallback.py +163 -1
  10. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/pyproject.toml +1 -1
  11. coderouter_cli-2.3.0a3/tests/test_plugins_integration.py +303 -0
  12. coderouter_cli-2.3.0a3/tests/test_plugins_loader.py +261 -0
  13. coderouter_cli-2.3.0a3/tests/test_plugins_registry.py +90 -0
  14. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/.gitignore +0 -0
  15. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/LICENSE +0 -0
  16. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/README.en.md +0 -0
  17. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/README.md +0 -0
  18. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/__init__.py +0 -0
  19. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/__main__.py +0 -0
  20. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/adapters/__init__.py +0 -0
  21. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/adapters/anthropic_native.py +0 -0
  22. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/adapters/base.py +0 -0
  23. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/adapters/openai_compat.py +0 -0
  24. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/adapters/registry.py +0 -0
  25. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/cli.py +0 -0
  26. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/cli_stats.py +0 -0
  27. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/config/__init__.py +0 -0
  28. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/config/capability_registry.py +0 -0
  29. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/config/env_file.py +0 -0
  30. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/config/loader.py +0 -0
  31. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/cost.py +0 -0
  32. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/data/__init__.py +0 -0
  33. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/data/model-capabilities.yaml +0 -0
  34. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/doctor.py +0 -0
  35. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/doctor_apply.py +0 -0
  36. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/env_security.py +0 -0
  37. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/errors.py +0 -0
  38. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/guards/__init__.py +0 -0
  39. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/guards/backend_health.py +0 -0
  40. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/guards/context_budget.py +0 -0
  41. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/guards/continuous_probe.py +0 -0
  42. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/guards/drift_actions.py +0 -0
  43. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/guards/drift_detection.py +0 -0
  44. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/guards/memory_pressure.py +0 -0
  45. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/guards/self_healing.py +0 -0
  46. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/guards/tool_loop.py +0 -0
  47. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/ingress/__init__.py +0 -0
  48. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/ingress/anthropic_routes.py +0 -0
  49. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/ingress/dashboard_routes.py +0 -0
  50. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/ingress/metrics_routes.py +0 -0
  51. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/ingress/openai_routes.py +0 -0
  52. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/logging.py +0 -0
  53. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/metrics/__init__.py +0 -0
  54. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/metrics/collector.py +0 -0
  55. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/metrics/prometheus.py +0 -0
  56. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/output_filters.py +0 -0
  57. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/routing/__init__.py +0 -0
  58. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/routing/adaptive.py +0 -0
  59. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/routing/auto_router.py +0 -0
  60. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/routing/budget.py +0 -0
  61. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/routing/capability.py +0 -0
  62. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/state/__init__.py +0 -0
  63. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/state/audit_log.py +0 -0
  64. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/state/replay.py +0 -0
  65. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/state/request_log.py +0 -0
  66. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/state/store.py +0 -0
  67. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/token_estimation.py +0 -0
  68. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/translation/__init__.py +0 -0
  69. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/translation/anthropic.py +0 -0
  70. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/translation/convert.py +0 -0
  71. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/coderouter/translation/tool_repair.py +0 -0
  72. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/architecture.md +0 -0
  73. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/assets/dashboard-demo.png +0 -0
  74. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/context-budget.md +0 -0
  75. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/continuous-probing.md +0 -0
  76. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  77. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/designs/v1.6-auto-router-verification.md +0 -0
  78. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/designs/v1.6-auto-router.md +0 -0
  79. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/drift-detection.md +0 -0
  80. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/free-tier-guide.en.md +0 -0
  81. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/free-tier-guide.md +0 -0
  82. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/gguf_dl.md +0 -0
  83. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/hf-ollama-models.md +0 -0
  84. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/llamacpp-direct.en.md +0 -0
  85. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/llamacpp-direct.md +0 -0
  86. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/lmstudio-direct.en.md +0 -0
  87. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/lmstudio-direct.md +0 -0
  88. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/openrouter-roster/CHANGES.md +0 -0
  89. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/openrouter-roster/README.md +0 -0
  90. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/openrouter-roster/latest.json +0 -0
  91. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/partial-stitch.md +0 -0
  92. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/quickstart.en.md +0 -0
  93. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/quickstart.md +0 -0
  94. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/retrospectives/v0.4.md +0 -0
  95. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/retrospectives/v0.5-verify.md +0 -0
  96. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/retrospectives/v0.5.md +0 -0
  97. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/retrospectives/v0.6.md +0 -0
  98. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/retrospectives/v0.7.md +0 -0
  99. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/retrospectives/v1.0-verify.md +0 -0
  100. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/retrospectives/v1.0.md +0 -0
  101. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/security.en.md +0 -0
  102. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/security.md +0 -0
  103. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/troubleshooting.en.md +0 -0
  104. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/troubleshooting.md +0 -0
  105. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/usage-guide.en.md +0 -0
  106. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/usage-guide.md +0 -0
  107. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/when-do-i-need-coderouter.en.md +0 -0
  108. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/docs/when-do-i-need-coderouter.md +0 -0
  109. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/examples/.env.example +0 -0
  110. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/examples/providers.auto-custom.yaml +0 -0
  111. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/examples/providers.auto.yaml +0 -0
  112. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/examples/providers.note-2026.yaml +0 -0
  113. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/examples/providers.nvidia-nim.yaml +0 -0
  114. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/examples/providers.raspberrypi.yaml +0 -0
  115. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/examples/providers.v2-context-budget.yaml +0 -0
  116. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/examples/providers.yaml +0 -0
  117. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/scripts/demo_traffic.sh +0 -0
  118. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/scripts/openrouter_roster_diff.py +0 -0
  119. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/scripts/smoke_v2_2.sh +0 -0
  120. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/scripts/verify_v0_5.sh +0 -0
  121. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/scripts/verify_v1_0.sh +0 -0
  122. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/__init__.py +0 -0
  123. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/conftest.py +0 -0
  124. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_adapter_anthropic.py +0 -0
  125. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_audit_log.py +0 -0
  126. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_auto_router.py +0 -0
  127. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_backend_health.py +0 -0
  128. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_budget.py +0 -0
  129. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_capability.py +0 -0
  130. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_capability_degraded_payload.py +0 -0
  131. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_capability_registry.py +0 -0
  132. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_capability_registry_cache_control.py +0 -0
  133. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_claude_code_suitability.py +0 -0
  134. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_cli.py +0 -0
  135. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_cli_stats.py +0 -0
  136. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_config.py +0 -0
  137. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_context_budget.py +0 -0
  138. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_continuous_probe.py +0 -0
  139. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_dashboard_endpoint.py +0 -0
  140. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_doctor.py +0 -0
  141. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_doctor_apply.py +0 -0
  142. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_doctor_cache_probe.py +0 -0
  143. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_drift_actions.py +0 -0
  144. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_drift_detection.py +0 -0
  145. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_drift_detection_integration.py +0 -0
  146. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_env_file.py +0 -0
  147. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_env_security.py +0 -0
  148. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_errors.py +0 -0
  149. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_examples_yaml.py +0 -0
  150. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_fallback.py +0 -0
  151. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_fallback_anthropic.py +0 -0
  152. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_fallback_cache_control.py +0 -0
  153. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_fallback_cache_observed.py +0 -0
  154. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_fallback_misconfig_warn.py +0 -0
  155. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_fallback_paid_gate.py +0 -0
  156. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_fallback_thinking.py +0 -0
  157. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_guards_tool_loop.py +0 -0
  158. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_ingress_anthropic.py +0 -0
  159. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_ingress_profile.py +0 -0
  160. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_memory_pressure.py +0 -0
  161. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_metrics_cache.py +0 -0
  162. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_metrics_collector.py +0 -0
  163. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_metrics_cost.py +0 -0
  164. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_metrics_endpoint.py +0 -0
  165. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_metrics_jsonl.py +0 -0
  166. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_metrics_prometheus.py +0 -0
  167. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_metrics_prometheus_cache.py +0 -0
  168. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_openai_compat.py +0 -0
  169. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_openrouter_roster_diff.py +0 -0
  170. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_output_filters.py +0 -0
  171. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_output_filters_adapters.py +0 -0
  172. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_partial_stitch.py +0 -0
  173. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_reasoning_strip.py +0 -0
  174. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_request_log.py +0 -0
  175. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_routing_adaptive.py +0 -0
  176. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_self_healing.py +0 -0
  177. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_setup_sh.py +0 -0
  178. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_state_store.py +0 -0
  179. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_token_estimation.py +0 -0
  180. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_tool_repair.py +0 -0
  181. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_translation_anthropic.py +0 -0
  182. {coderouter_cli-2.2.0 → coderouter_cli-2.3.0a3}/tests/test_translation_reverse.py +0 -0
@@ -6,6 +6,145 @@ versioning follows [SemVer](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [v2.3.0a3] — 2026-05-08 (Plugin SDK — LogRecord.module collision fix)
10
+
11
+ Patch over `v2.3.0a2`. The wheel-install-and-test job in CI surfaced
12
+ one more issue that the source-tree test runs hadn't caught.
13
+
14
+ ### Fixed
15
+
16
+ - **`KeyError: "Attempt to overwrite 'module' in LogRecord"`** in
17
+ `discover_and_load`. Python's `logging` module reserves several
18
+ attribute names on `LogRecord` (``name`` / ``msg`` / ``args`` /
19
+ ``levelname`` / ``levelno`` / ``pathname`` / ``filename`` /
20
+ **``module``** / ``lineno`` / ``funcName`` / ``exc_info`` /
21
+ ``exc_text`` / ``stack_info`` / ``created`` / ``msecs`` /
22
+ ``relativeCreated`` / ``thread`` / ``threadName`` / ``processName``
23
+ / ``process`` / ``message`` / ``asctime``); passing any of these
24
+ via ``extra=`` raises ``KeyError`` rather than silently overwriting.
25
+ v2.3.0a1's `plugin-loaded` and `plugin-load-failed` log lines used
26
+ ``"module"`` as an extra key (intended to mean "the module:attr
27
+ string from `entry_point.value`"), which collided.
28
+
29
+ Renamed the key to ``"entry_point"`` everywhere in
30
+ `coderouter/plugins/loader.py`. Audited every `extra=` payload in
31
+ the new plugins module + the engine's hook helpers — none of them
32
+ use any of the other reserved names.
33
+
34
+ ### Files touched
35
+
36
+ ```
37
+ M coderouter/plugins/loader.py — "module" → "entry_point" (×2)
38
+ M CHANGELOG.md, pyproject.toml — 2.3.0a2 → 2.3.0a3
39
+ ```
40
+
41
+ No other behavioral change. Downstream consumers that don't rely on
42
+ the structured log shape are unaffected; anyone parsing the JSON
43
+ log lines should rename `module` → `entry_point` to match.
44
+
45
+ ---
46
+
47
+ ## [v2.3.0a2] — 2026-05-08 (Plugin SDK — CI fixes)
48
+
49
+ Patch over `v2.3.0a1`. The Plugin SDK addition was sound but two
50
+ issues showed up in the test matrix and have been fixed:
51
+
52
+ ### Fixed
53
+
54
+ - **`'FallbackEngine' object has no attribute 'plugins'`** —
55
+ Many existing tests construct the engine via
56
+ ``FallbackEngine.__new__`` to bypass full initialization (only
57
+ ``config`` + ``_adapters`` are populated). The new direct
58
+ attribute ``self.plugins`` was missing on those instances and
59
+ raised ``AttributeError`` whenever the engine reached the hook
60
+ helpers. Converted to the same lazy-property pattern that
61
+ ``_adaptive`` / ``_budget`` / ``_memory_pressure_guard`` already
62
+ use: store under ``_plugin_registry`` in ``__init__``, surface
63
+ via a ``plugins`` property that lazily builds an empty registry
64
+ when the underlying attribute is missing. Bypass-tests now see
65
+ an empty registry and the hook helpers short-circuit cleanly.
66
+
67
+ - **`LogRecord` assertions in `test_plugins_loader.py` /
68
+ `test_plugins_integration.py`** — used ``rec.message`` which
69
+ isn't always populated (depends on whether a Formatter has
70
+ processed the record). Switched to ``rec.msg`` with exact
71
+ match, matching the rest of the test suite's convention
72
+ (e.g. ``test_fallback_paid_gate.py``,
73
+ ``test_memory_pressure.py``) where structured-log event names
74
+ are tested via ``rec.msg == "<event-name>"``.
75
+
76
+ ### Files touched
77
+
78
+ ```
79
+ M coderouter/routing/fallback.py — lazy plugins property
80
+ M tests/test_plugins_loader.py — rec.msg ==
81
+ M tests/test_plugins_integration.py — rec.msg ==
82
+ M CHANGELOG.md, pyproject.toml — 2.3.0a1 → 2.3.0a2
83
+ ```
84
+
85
+ No behavioral change vs `v2.3.0a1`. If you've already pinned
86
+ `v2.3.0a1` in a downstream that doesn't construct engines via
87
+ ``__new__`` and doesn't run our test suite, the upgrade is a
88
+ no-op for runtime.
89
+
90
+ ---
91
+
92
+ ## [v2.3.0a1] — 2026-05-08 (Plugin SDK)
93
+
94
+ **Theme: in-process plugin SDK. Core 5 deps stays untouched.** v2.3.0a1 adds the plugin discovery + dispatch infrastructure that ``coderouter-plugin-memory`` 0.1.0+ will consume. Two of the six designed extension points (``input_filter`` and ``observer``) are wired into the engine; the other four (``frontend`` / ``guard`` / ``output_filter`` / ``adapter``) ship as Protocol contracts only — plugin authors can target them today, but engine integration is deferred until a real plugin drives the requirement (v2.4+).
95
+
96
+ ### Plugin SDK (new module: ``coderouter.plugins``)
97
+
98
+ | Component | What it does |
99
+ |---|---|
100
+ | ``coderouter.plugins.base`` | Six ``Protocol`` definitions (InputFilter, Observer, Frontend, Guard, OutputFilter, Adapter). All ``runtime_checkable`` so ``isinstance(x, InputFilter)`` works for diagnostics. |
101
+ | ``coderouter.plugins.loader`` | Reads ``importlib.metadata.entry_points`` under ``coderouter.<group>`` and applies the user's explicit ``plugins.enabled`` allowlist. Failures are logged + degraded — never abort startup. |
102
+ | ``coderouter.plugins.registry`` | Group-keyed container. ``input_filters`` / ``observers`` properties return defensive copies. |
103
+ | ``PluginsConfig`` (in ``schemas.py``) | New ``plugins:`` block in ``providers.yaml`` — ``enabled`` list + ``config`` dict. Absent → identical behavior to v2.2.0. |
104
+ | ``FallbackEngine`` integration | ``__init__`` now takes ``plugins=PluginRegistry``; ``generate_anthropic`` runs the InputFilter chain before chain dispatch; both Anthropic paths fan out ``request_completed`` to observers as fire-and-forget asyncio tasks. The no-plugin code path is bit-identical to v2.2.0. |
105
+ | ``ingress/app.py`` | ``create_app`` calls ``discover_and_load`` and hands the registry to the engine. |
106
+
107
+ ### Supply-chain defense
108
+
109
+ ``pip install coderouter-plugin-X`` is **not** sufficient to activate a plugin. The user must also list its entry-point name under ``plugins.enabled`` in ``providers.yaml``. Unlisted-but-installed entry points are logged ``plugin-skipped`` and never instantiated, so a compromised transitive dependency cannot wedge itself into the request flow.
110
+
111
+ ### Failure semantics
112
+
113
+ | Failure | Engine behavior |
114
+ |---|---|
115
+ | ``importlib.metadata`` finds no entry point with an enabled name | ``plugin-not-found`` warn (one per missing name); engine boots normally. |
116
+ | Plugin module import fails | ``plugin-load-failed`` error; engine boots without that plugin. |
117
+ | Plugin ``__init__`` raises | Same — error logged, plugin skipped. |
118
+ | ``InputFilter.transform`` raises | ``input-filter-failed`` warn; pre-mutation request flows to the next filter / chain. |
119
+ | ``Observer.on_event`` raises | ``observer-failed`` warn; engine response is unaffected (fanout is fire-and-forget). |
120
+
121
+ ### Backward compatibility
122
+
123
+ 100%. ``providers.yaml`` files written for v2.2.0 keep working unchanged because ``plugins`` is optional and defaults to ``None``. The ``FallbackEngine(config)`` legacy constructor keeps working too — the new ``plugins=`` parameter has a sane default.
124
+
125
+ ### Files changed
126
+
127
+ ```
128
+ A coderouter/plugins/__init__.py
129
+ A coderouter/plugins/base.py
130
+ A coderouter/plugins/loader.py
131
+ A coderouter/plugins/registry.py
132
+ M coderouter/config/schemas.py — PluginsConfig + CodeRouterConfig.plugins field
133
+ M coderouter/routing/fallback.py — engine __init__, _apply_input_filters,
134
+ _fanout_observers, _safe_observe + hook calls
135
+ M coderouter/ingress/app.py — discover_and_load wired into create_app
136
+ A tests/test_plugins_registry.py
137
+ A tests/test_plugins_loader.py
138
+ A tests/test_plugins_integration.py
139
+ ```
140
+
141
+ ### Out-of-scope (deferred)
142
+
143
+ - Engine integration for ``frontend`` / ``guard`` / ``output_filter`` / ``adapter`` — Protocol contracts only.
144
+ - ``coderouter-plugin-memory`` itself — separate repo, separate release cadence (0.1.0 lands after this Core release publishes).
145
+
146
+ ---
147
+
9
148
  ## [v2.2.0] — 2026-05-06 (Self-healing + Multi-day operation + Replay)
10
149
 
11
150
  **Theme: 自己修復 + 状態永続化 + 統計リプレイで "無人長時間運用" 基盤を完成。** v2.0-J で UNHEALTHY provider の自動除外 + restart + 復帰を実装、v2.0-K で sqlite3 StateStore + 構造化 audit log + request journal + `coderouter replay` 統計 A/B 分析を実装。v2.2 で Unsloth Studio 由来の堅牢化 3 件を吸収。**6 系統障害全対処 + 自己修復 + 永続化 + リプレイ**に到達。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.2.0
3
+ Version: 2.3.0a3
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
@@ -14,7 +14,7 @@ Design notes (see plan.md §2 / §5.4):
14
14
  from __future__ import annotations
15
15
 
16
16
  import re
17
- from typing import Literal, Self
17
+ from typing import Any, Literal, Self
18
18
 
19
19
  from pydantic import BaseModel, ConfigDict, Field, HttpUrl, model_validator
20
20
 
@@ -852,6 +852,51 @@ class AutoRouterConfig(BaseModel):
852
852
  )
853
853
 
854
854
 
855
+ class PluginsConfig(BaseModel):
856
+ """The ``plugins:`` block in providers.yaml (v2.3.0).
857
+
858
+ Declarative opt-in for in-process plugins distributed as separate
859
+ PyPI packages (``coderouter-plugin-*``). Two-step gating:
860
+
861
+ 1. ``pip install coderouter-plugin-X`` makes the entry point
862
+ discoverable.
863
+ 2. The plugin's entry-point name MUST appear in :attr:`enabled`
864
+ before the loader will instantiate it.
865
+
866
+ Step 2 is the supply-chain defense: a malicious transitive dep
867
+ cannot wedge itself into the request flow without an explicit
868
+ user action in providers.yaml. See
869
+ :mod:`coderouter.plugins.loader` for the full discovery logic.
870
+ """
871
+
872
+ model_config = ConfigDict(extra="forbid")
873
+
874
+ enabled: list[str] = Field(
875
+ default_factory=list,
876
+ description=(
877
+ "v2.3.0: ordered list of plugin entry-point names to load. "
878
+ "An entry-point name is the LHS of an entry in a plugin's "
879
+ "``[project.entry-points.\"coderouter.<group>\"]`` block — "
880
+ "e.g. ``memory`` for ``coderouter-plugin-memory``. Order "
881
+ "controls the order InputFilter chains apply (each filter "
882
+ "sees the previous filter's output). Empty list = no "
883
+ "plugins active (default behavior, identical to v2.2.0)."
884
+ ),
885
+ )
886
+ config: dict[str, dict[str, Any]] = Field(
887
+ default_factory=dict,
888
+ description=(
889
+ "v2.3.0: per-plugin keyword arguments. The dict at "
890
+ "``config[<plugin-name>]`` is splatted into the plugin's "
891
+ "``__init__`` as ``**kwargs``. Validation of each "
892
+ "sub-dict's schema is the plugin's responsibility — Core "
893
+ "stays out of plugin-specific config shapes. Plugins not "
894
+ "listed in :attr:`enabled` are ignored even if they have "
895
+ "config entries here."
896
+ ),
897
+ )
898
+
899
+
855
900
  class CodeRouterConfig(BaseModel):
856
901
  """Top-level config loaded from providers.yaml."""
857
902
 
@@ -1001,6 +1046,21 @@ class CodeRouterConfig(BaseModel):
1001
1046
  ),
1002
1047
  )
1003
1048
 
1049
+ # v2.3.0: in-process plugin SDK. Optional — when None, the engine
1050
+ # builds an empty ``PluginRegistry`` and the hook chains are
1051
+ # short-circuited (zero-cost path, identical to v2.2.0 behavior).
1052
+ plugins: PluginsConfig | None = Field(
1053
+ default=None,
1054
+ description=(
1055
+ "v2.3.0: in-process plugin configuration. Plugins are "
1056
+ "distributed as separate PyPI packages (e.g. "
1057
+ "``coderouter-plugin-memory``); this block lists which of "
1058
+ "the installed plugins to actually activate, and supplies "
1059
+ "their per-plugin keyword arguments. Absent or empty = no "
1060
+ "plugins (zero-cost, backward-compatible default)."
1061
+ ),
1062
+ )
1063
+
1004
1064
  @model_validator(mode="after")
1005
1065
  def _check_default_profile_exists(self) -> CodeRouterConfig:
1006
1066
  """v0.6-A: surface a typo'd ``default_profile`` at load time.
@@ -17,6 +17,7 @@ from coderouter.ingress.metrics_routes import router as metrics_router
17
17
  from coderouter.ingress.openai_routes import router as openai_router
18
18
  from coderouter.logging import configure_logging, get_logger
19
19
  from coderouter.metrics import install_collector
20
+ from coderouter.plugins import discover_and_load
20
21
  from coderouter.routing import FallbackEngine
21
22
  from coderouter.routing.capability import check_claude_code_chain_suitability
22
23
 
@@ -38,7 +39,13 @@ def create_app(config_path: str | None = None) -> FastAPI:
38
39
  # so multiple create_app() calls (tests) don't stack handlers.
39
40
  install_collector()
40
41
  config = load_config(config_path)
41
- engine = FallbackEngine(config)
42
+ # v2.3.0: discover plugins from importlib.metadata entry points and
43
+ # apply the user's explicit ``plugins.enabled`` allowlist. When the
44
+ # ``plugins`` block is absent or empty, the loader returns an empty
45
+ # registry and the engine's hook loops short-circuit — the request
46
+ # flow is bit-identical to v2.2.0 in that default case.
47
+ plugin_registry = discover_and_load(config)
48
+ engine = FallbackEngine(config, plugins=plugin_registry)
42
49
 
43
50
  @asynccontextmanager
44
51
  async def lifespan(app: FastAPI) -> AsyncIterator[None]:
@@ -0,0 +1,59 @@
1
+ """Plugin SDK — extension points for in-process CodeRouter plugins (v2.3.0).
2
+
3
+ CodeRouter Core stays at 5 deps. Optional functionality (memory, PII
4
+ redaction, observability bridges, alternative ingresses, etc.) ships
5
+ as separate ``coderouter-plugin-*`` packages on PyPI. Each plugin
6
+ declares one or more *entry points* in its ``pyproject.toml``; this
7
+ SDK discovers them at startup, applies the user's explicit ``enabled``
8
+ allowlist (supply chain defense, see :func:`loader.discover_and_load`),
9
+ and exposes a :class:`registry.PluginRegistry` to the engine.
10
+
11
+ Six extension points are defined as :mod:`Protocols <typing>` in
12
+ :mod:`coderouter.plugins.base`. v2.3.0 implements the engine-side hook
13
+ integration for two of them (``input_filter`` and ``observer``); the
14
+ remaining four (``frontend``, ``guard``, ``output_filter``,
15
+ ``adapter``) have a stable Protocol contract so plugins can target
16
+ them now, but the engine doesn't yet wire them into the request flow.
17
+ That's intentional — adding contracts is cheap, adding hot-path code
18
+ isn't, so we wait for a real plugin to drive each integration.
19
+
20
+ Public API:
21
+
22
+ - :class:`InputFilter`, :class:`Observer` — implementable today.
23
+ - :class:`Frontend`, :class:`Guard`, :class:`OutputFilter`,
24
+ :class:`Adapter` — Protocol-only, integration in v2.4+.
25
+ - :func:`discover_and_load` — called once during config load.
26
+ - :class:`PluginRegistry` — held by :class:`FallbackEngine`.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ from coderouter.plugins.base import (
31
+ Adapter,
32
+ Frontend,
33
+ Guard,
34
+ InputFilter,
35
+ Observer,
36
+ OutputFilter,
37
+ )
38
+ from coderouter.plugins.loader import (
39
+ PLUGIN_GROUPS_FUTURE,
40
+ PLUGIN_GROUPS_V2_3,
41
+ discover_and_load,
42
+ )
43
+ from coderouter.plugins.registry import PluginRegistry
44
+
45
+ __all__ = [
46
+ # Active hooks
47
+ "InputFilter",
48
+ "Observer",
49
+ # Future hooks (Protocol-only, no engine integration yet)
50
+ "Frontend",
51
+ "Guard",
52
+ "OutputFilter",
53
+ "Adapter",
54
+ # Discovery + container
55
+ "PluginRegistry",
56
+ "discover_and_load",
57
+ "PLUGIN_GROUPS_V2_3",
58
+ "PLUGIN_GROUPS_FUTURE",
59
+ ]
@@ -0,0 +1,168 @@
1
+ """Plugin SDK Protocol contracts (v2.3.0).
2
+
3
+ Six extension points are defined here. Two are wired into the engine
4
+ in v2.3.0 (:class:`InputFilter`, :class:`Observer`); four are
5
+ Protocol-only (:class:`Frontend`, :class:`Guard`, :class:`OutputFilter`,
6
+ :class:`Adapter`) and will get engine integration when a real plugin
7
+ drives the requirement — see ``docs/inside/plugin-architecture-draft.md``
8
+ §3 for the full design rationale.
9
+
10
+ Why declare contracts ahead of integration? It lets a plugin author
11
+ build against the SDK *now* (and ship a working ``coderouter.frontend``
12
+ plugin once integration lands) without us having to do a
13
+ backward-incompatible Protocol revision later. ``runtime_checkable``
14
+ is used so :func:`isinstance` checks work in the loader for clearer
15
+ error messages.
16
+
17
+ All hooks are async. Failures must NEVER block the engine response —
18
+ the engine wraps every hook call in try/except and degrades gracefully
19
+ (see ``coderouter/routing/fallback.py`` integration site).
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
24
+
25
+ if TYPE_CHECKING:
26
+ # Avoid circular imports at runtime — Protocol typing only needs
27
+ # these for documentation and static analysis.
28
+ from coderouter.config.schemas import CodeRouterConfig
29
+ from coderouter.translation.anthropic import (
30
+ AnthropicRequest,
31
+ AnthropicResponse,
32
+ )
33
+
34
+
35
+ # ====================================================================
36
+ # Active hooks (engine integration in v2.3.0)
37
+ # ====================================================================
38
+
39
+
40
+ @runtime_checkable
41
+ class InputFilter(Protocol):
42
+ """Mutates an inbound :class:`AnthropicRequest` before chain dispatch.
43
+
44
+ Plugins MUST treat the input as immutable: build the modified
45
+ request with ``request.model_copy(update={...})`` and return the
46
+ new instance. The engine assumes the returned value is a
47
+ *replacement*, not the same object.
48
+
49
+ Engine semantics:
50
+
51
+ - Filters run sequentially in the order they appear in
52
+ ``plugins.enabled``. The first filter sees the raw inbound
53
+ request; each subsequent filter sees the previous filter's
54
+ output.
55
+ - If :meth:`transform` raises, the engine logs ``input-filter-failed``
56
+ and continues with the *pre-mutation* request for that filter.
57
+ Other filters still run.
58
+ - The transform runs *after* the v1.9-E tool-loop guard but
59
+ *before* chain resolution and the v2.0-F context budget guard.
60
+ That order matters: filters can grow ``request.system`` (e.g.
61
+ memory injection) without bypassing the budget cap, because the
62
+ budget guard reruns over the post-filter payload.
63
+ """
64
+
65
+ name: str
66
+
67
+ async def transform(self, request: AnthropicRequest) -> AnthropicRequest: ...
68
+
69
+
70
+ @runtime_checkable
71
+ class Observer(Protocol):
72
+ """Passive event consumer. Cannot mutate anything.
73
+
74
+ The engine calls observers via :func:`asyncio.create_task` (fire
75
+ and forget). An observer that takes 30s won't slow a 200ms
76
+ response down — it just runs in the background. If it raises,
77
+ the engine logs ``observer-failed`` and discards the exception.
78
+
79
+ Event vocabulary (v2.3.0):
80
+
81
+ - ``request_completed`` — payload: ``{request, response,
82
+ latency_ms, provider}``. Fires after a successful Anthropic or
83
+ OpenAI-compat response. Streaming requests fire this event once,
84
+ after the SSE stream has terminated (not on each chunk).
85
+
86
+ Plugins MUST tolerate unknown event types — the vocabulary will
87
+ grow over time, and an old plugin should silently ignore events
88
+ it doesn't recognize rather than crash.
89
+ """
90
+
91
+ name: str
92
+
93
+ async def on_event(self, event_type: str, payload: dict[str, Any]) -> None: ...
94
+
95
+
96
+ # ====================================================================
97
+ # Future hooks (Protocol-only, engine integration in v2.4+)
98
+ # ====================================================================
99
+
100
+
101
+ @runtime_checkable
102
+ class Frontend(Protocol):
103
+ """Alternative ingress (Discord, Telegram, MCP, Voice, ...).
104
+
105
+ Frontends *run* the engine rather than being called by it.
106
+ Integration plan: each frontend gets its own
107
+ :func:`asyncio.create_task` started by ``coderouter serve``;
108
+ SIGTERM cancels all of them and waits for cleanup.
109
+
110
+ Not yet integrated — Protocol contract only.
111
+ """
112
+
113
+ name: str
114
+
115
+ async def serve(
116
+ self, engine: Any, config: CodeRouterConfig
117
+ ) -> None: ...
118
+
119
+
120
+ @runtime_checkable
121
+ class Guard(Protocol):
122
+ """Reliability guard, parallel to the built-in tool-loop guard.
123
+
124
+ Runs synchronously on the hot path, so implementations must be
125
+ cheap. Heavy work (HTTP calls, model invocations, etc.) belongs
126
+ in an :class:`Observer`.
127
+
128
+ Not yet integrated — Protocol contract only.
129
+ """
130
+
131
+ name: str
132
+
133
+ async def check(
134
+ self, request: AnthropicRequest, config: CodeRouterConfig
135
+ ) -> None: ...
136
+
137
+
138
+ @runtime_checkable
139
+ class OutputFilter(Protocol):
140
+ """Mutates a response before it returns to the client.
141
+
142
+ For streaming, the engine plans to call :meth:`transform` once
143
+ per ``AnthropicStreamEvent``; for non-streaming, once per
144
+ response.
145
+
146
+ Not yet integrated — Protocol contract only.
147
+ """
148
+
149
+ name: str
150
+
151
+ async def transform(
152
+ self, response: AnthropicResponse
153
+ ) -> AnthropicResponse: ...
154
+
155
+
156
+ @runtime_checkable
157
+ class Adapter(Protocol):
158
+ """New ``kind`` value in providers.yaml (e.g. ``bedrock-native``).
159
+
160
+ Plugins implement the same async surface as
161
+ :class:`coderouter.adapters.base.BaseAdapter` so the engine can
162
+ treat them indistinguishably from built-in adapters once the
163
+ loader registers the new ``kind`` mapping.
164
+
165
+ Not yet integrated — Protocol contract only.
166
+ """
167
+
168
+ name: str
@@ -0,0 +1,176 @@
1
+ """Plugin discovery + instantiation (v2.3.0).
2
+
3
+ Reads ``importlib.metadata`` entry points under the ``coderouter.*``
4
+ groups, applies the user's explicit ``plugins.enabled`` allowlist,
5
+ constructs each plugin with its config dict, and returns a
6
+ :class:`PluginRegistry` ready for the engine to consume.
7
+
8
+ Supply-chain defense
9
+ ====================
10
+
11
+ Just having ``coderouter-plugin-X`` installed is **not enough** to
12
+ activate a plugin — the user must list the entry-point name in
13
+ ``providers.yaml`` under ``plugins.enabled``. Without that explicit
14
+ opt-in, an installed-but-unlisted plugin is silently skipped (logged
15
+ as ``plugin-skipped``). This stops a compromised transitive
16
+ dependency from injecting itself into the request flow.
17
+
18
+ Failure mode: degraded continue
19
+ ===============================
20
+
21
+ Plugin loading is best-effort. A failure to import a module, find a
22
+ class, or call ``__init__`` is logged at error level and the engine
23
+ keeps booting without that plugin. The rationale: a misconfigured
24
+ optional plugin shouldn't take down the wire-level router that other
25
+ plugins (and the core) depend on. Operators see the failure in logs
26
+ and the ``/dashboard`` plugin panel.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import importlib.metadata as md
31
+ from typing import TYPE_CHECKING
32
+
33
+ from coderouter.logging import get_logger
34
+ from coderouter.plugins.registry import PluginRegistry
35
+
36
+ if TYPE_CHECKING:
37
+ from coderouter.config.schemas import CodeRouterConfig
38
+
39
+ logger = get_logger(__name__)
40
+
41
+ # Active hook groups in v2.3.0. The engine wires these into the
42
+ # request flow; plugins targeting them will see their methods called
43
+ # at runtime.
44
+ PLUGIN_GROUPS_V2_3: tuple[str, ...] = ("input_filter", "observer")
45
+
46
+ # Hook groups whose Protocol contracts are stable but whose engine
47
+ # integration is deferred. Listing them here means a plugin author
48
+ # can publish today and the loader will silently skip them with a
49
+ # clear log line — no surprise crashes when integration lands.
50
+ PLUGIN_GROUPS_FUTURE: tuple[str, ...] = (
51
+ "frontend",
52
+ "guard",
53
+ "output_filter",
54
+ "adapter",
55
+ )
56
+
57
+
58
+ def discover_and_load(config: CodeRouterConfig) -> PluginRegistry:
59
+ """Load plugins listed in ``config.plugins.enabled``.
60
+
61
+ Returns an empty registry when the ``plugins`` block is absent
62
+ from providers.yaml or its ``enabled`` list is empty. The empty
63
+ return is the same one ``PluginRegistry.empty()`` produces, so
64
+ the engine's hook loops short-circuit and incur zero cost in the
65
+ default (no-plugin) configuration.
66
+
67
+ Each enabled plugin is instantiated as
68
+ ``cls(**config.plugins.config.get(name, {}))``. The plugin's
69
+ ``__init__`` gets a fresh dict (validation happens inside the
70
+ plugin's own constructor — Core stays out of plugin-specific
71
+ schemas).
72
+
73
+ Logs emitted (one per outcome, all structured):
74
+
75
+ - ``plugin-loaded`` (info) — module + class loaded and constructed.
76
+ - ``plugin-skipped`` (info) — entry point exists but not enabled.
77
+ - ``plugin-load-failed`` (error) — import or ``__init__`` raised;
78
+ the engine continues without that plugin.
79
+ """
80
+ plugins_cfg = getattr(config, "plugins", None)
81
+ if plugins_cfg is None or not plugins_cfg.enabled:
82
+ return PluginRegistry.empty()
83
+
84
+ enabled = set(plugins_cfg.enabled)
85
+ registry = PluginRegistry()
86
+
87
+ # Track which enabled names actually matched an entry point so we
88
+ # can warn the operator about typos in ``plugins.enabled`` —
89
+ # otherwise a misspelled name silently does nothing and is hard
90
+ # to diagnose.
91
+ seen_names: set[str] = set()
92
+
93
+ for group in PLUGIN_GROUPS_V2_3 + PLUGIN_GROUPS_FUTURE:
94
+ ep_group = f"coderouter.{group}"
95
+ for ep in md.entry_points(group=ep_group):
96
+ if ep.name not in enabled:
97
+ # Installed but not enabled — silently skip. Logged
98
+ # at info level so operators have a paper trail of
99
+ # plugins they could enable.
100
+ logger.info(
101
+ "plugin-skipped",
102
+ extra={
103
+ "plugin": ep.name,
104
+ "group": group,
105
+ "reason": "not in plugins.enabled",
106
+ },
107
+ )
108
+ continue
109
+
110
+ seen_names.add(ep.name)
111
+
112
+ # Future-only groups: plugin is installed *and* enabled,
113
+ # but the engine doesn't yet wire this group into the
114
+ # request flow. Load it anyway so the plugin can sanity-
115
+ # check its own construction; just log a clear warning.
116
+ if group in PLUGIN_GROUPS_FUTURE:
117
+ logger.warning(
118
+ "plugin-group-not-yet-active",
119
+ extra={
120
+ "plugin": ep.name,
121
+ "group": group,
122
+ "note": (
123
+ f"engine integration for '{group}' "
124
+ "is deferred to v2.4+; the plugin will "
125
+ "be loaded but its hook is never called"
126
+ ),
127
+ },
128
+ )
129
+
130
+ try:
131
+ cls = ep.load()
132
+ cfg = plugins_cfg.config.get(ep.name, {}) or {}
133
+ instance = cls(**cfg)
134
+ registry.add(group, instance)
135
+ logger.info(
136
+ "plugin-loaded",
137
+ extra={
138
+ "plugin": ep.name,
139
+ "group": group,
140
+ "entry_point": ep.value,
141
+ },
142
+ )
143
+ except Exception as exc:
144
+ # Don't propagate — engine boot must succeed even when
145
+ # an optional plugin is broken.
146
+ logger.error(
147
+ "plugin-load-failed",
148
+ extra={
149
+ "plugin": ep.name,
150
+ "group": group,
151
+ "entry_point": ep.value,
152
+ "error_type": type(exc).__name__,
153
+ "error": str(exc),
154
+ },
155
+ )
156
+
157
+ # Warn on enabled names that didn't match any installed entry
158
+ # point — most often this means the user typed a name in
159
+ # providers.yaml but forgot to ``pip install`` the corresponding
160
+ # plugin package.
161
+ missing = enabled - seen_names
162
+ if missing:
163
+ for name in sorted(missing):
164
+ logger.warning(
165
+ "plugin-not-found",
166
+ extra={
167
+ "plugin": name,
168
+ "hint": (
169
+ "listed in plugins.enabled but no entry point "
170
+ "with this name was discovered. Did you forget "
171
+ "to install the plugin package?"
172
+ ),
173
+ },
174
+ )
175
+
176
+ return registry