coderouter-cli 1.9.0__tar.gz → 1.10.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.
Files changed (144) hide show
  1. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/CHANGELOG.md +670 -1
  2. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/PKG-INFO +25 -8
  3. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/README.en.md +24 -7
  4. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/README.md +24 -7
  5. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/config/schemas.py +179 -1
  6. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/doctor.py +1 -1
  7. coderouter_cli-1.10.1/coderouter/guards/backend_health.py +208 -0
  8. coderouter_cli-1.10.1/coderouter/guards/memory_pressure.py +210 -0
  9. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/logging.py +352 -0
  10. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/metrics/collector.py +86 -0
  11. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/metrics/prometheus.py +84 -0
  12. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/routing/auto_router.py +165 -13
  13. coderouter_cli-1.10.1/coderouter/routing/budget.py +191 -0
  14. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/routing/fallback.py +594 -39
  15. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/llamacpp-direct.en.md +1 -1
  16. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/llamacpp-direct.md +1 -1
  17. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/lmstudio-direct.en.md +1 -1
  18. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/lmstudio-direct.md +1 -1
  19. coderouter_cli-1.10.1/examples/providers.raspberrypi.yaml +298 -0
  20. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/pyproject.toml +1 -1
  21. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_auto_router.py +519 -0
  22. coderouter_cli-1.10.1/tests/test_backend_health.py +413 -0
  23. coderouter_cli-1.10.1/tests/test_budget.py +351 -0
  24. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_fallback_cache_observed.py +147 -18
  25. coderouter_cli-1.10.1/tests/test_memory_pressure.py +362 -0
  26. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/.gitignore +0 -0
  27. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/LICENSE +0 -0
  28. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/__init__.py +0 -0
  29. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/__main__.py +0 -0
  30. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/adapters/__init__.py +0 -0
  31. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/adapters/anthropic_native.py +0 -0
  32. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/adapters/base.py +0 -0
  33. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/adapters/openai_compat.py +0 -0
  34. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/adapters/registry.py +0 -0
  35. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/cli.py +0 -0
  36. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/cli_stats.py +0 -0
  37. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/config/__init__.py +0 -0
  38. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/config/capability_registry.py +0 -0
  39. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/config/env_file.py +0 -0
  40. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/config/loader.py +0 -0
  41. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/cost.py +0 -0
  42. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/data/__init__.py +0 -0
  43. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/data/model-capabilities.yaml +0 -0
  44. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/doctor_apply.py +0 -0
  45. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/env_security.py +0 -0
  46. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/errors.py +0 -0
  47. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/guards/__init__.py +0 -0
  48. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/guards/tool_loop.py +0 -0
  49. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/ingress/__init__.py +0 -0
  50. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/ingress/anthropic_routes.py +0 -0
  51. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/ingress/app.py +0 -0
  52. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/ingress/dashboard_routes.py +0 -0
  53. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/ingress/metrics_routes.py +0 -0
  54. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/ingress/openai_routes.py +0 -0
  55. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/metrics/__init__.py +0 -0
  56. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/output_filters.py +0 -0
  57. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/routing/__init__.py +0 -0
  58. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/routing/adaptive.py +0 -0
  59. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/routing/capability.py +0 -0
  60. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/translation/__init__.py +0 -0
  61. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/translation/anthropic.py +0 -0
  62. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/translation/convert.py +0 -0
  63. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/coderouter/translation/tool_repair.py +0 -0
  64. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/assets/dashboard-demo.png +0 -0
  65. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  66. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/designs/v1.6-auto-router-verification.md +0 -0
  67. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/designs/v1.6-auto-router.md +0 -0
  68. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/free-tier-guide.en.md +0 -0
  69. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/free-tier-guide.md +0 -0
  70. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/hf-ollama-models.md +0 -0
  71. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/openrouter-roster/README.md +0 -0
  72. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/openrouter-roster/latest.json +0 -0
  73. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/quickstart.en.md +0 -0
  74. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/quickstart.md +0 -0
  75. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.4.md +0 -0
  76. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.5-verify.md +0 -0
  77. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.5.md +0 -0
  78. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.6.md +0 -0
  79. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.7.md +0 -0
  80. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/retrospectives/v1.0-verify.md +0 -0
  81. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/retrospectives/v1.0.md +0 -0
  82. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/security.en.md +0 -0
  83. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/security.md +0 -0
  84. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/troubleshooting.en.md +0 -0
  85. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/troubleshooting.md +0 -0
  86. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/usage-guide.en.md +0 -0
  87. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/usage-guide.md +0 -0
  88. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/when-do-i-need-coderouter.en.md +0 -0
  89. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/docs/when-do-i-need-coderouter.md +0 -0
  90. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/examples/.env.example +0 -0
  91. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/examples/providers.auto-custom.yaml +0 -0
  92. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/examples/providers.auto.yaml +0 -0
  93. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/examples/providers.note-2026.yaml +0 -0
  94. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/examples/providers.nvidia-nim.yaml +0 -0
  95. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/examples/providers.yaml +0 -0
  96. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/scripts/demo_traffic.sh +0 -0
  97. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/scripts/openrouter_roster_diff.py +0 -0
  98. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/scripts/verify_v0_5.sh +0 -0
  99. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/scripts/verify_v1_0.sh +0 -0
  100. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/__init__.py +0 -0
  101. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/conftest.py +0 -0
  102. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_adapter_anthropic.py +0 -0
  103. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_capability.py +0 -0
  104. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_capability_degraded_payload.py +0 -0
  105. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_capability_registry.py +0 -0
  106. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_capability_registry_cache_control.py +0 -0
  107. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_claude_code_suitability.py +0 -0
  108. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_cli.py +0 -0
  109. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_cli_stats.py +0 -0
  110. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_config.py +0 -0
  111. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_dashboard_endpoint.py +0 -0
  112. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_doctor.py +0 -0
  113. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_doctor_apply.py +0 -0
  114. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_doctor_cache_probe.py +0 -0
  115. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_env_file.py +0 -0
  116. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_env_security.py +0 -0
  117. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_errors.py +0 -0
  118. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_examples_yaml.py +0 -0
  119. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_fallback.py +0 -0
  120. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_fallback_anthropic.py +0 -0
  121. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_fallback_cache_control.py +0 -0
  122. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_fallback_misconfig_warn.py +0 -0
  123. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_fallback_paid_gate.py +0 -0
  124. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_fallback_thinking.py +0 -0
  125. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_guards_tool_loop.py +0 -0
  126. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_ingress_anthropic.py +0 -0
  127. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_ingress_profile.py +0 -0
  128. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_metrics_cache.py +0 -0
  129. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_metrics_collector.py +0 -0
  130. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_metrics_cost.py +0 -0
  131. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_metrics_endpoint.py +0 -0
  132. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_metrics_jsonl.py +0 -0
  133. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_metrics_prometheus.py +0 -0
  134. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_metrics_prometheus_cache.py +0 -0
  135. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_openai_compat.py +0 -0
  136. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_openrouter_roster_diff.py +0 -0
  137. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_output_filters.py +0 -0
  138. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_output_filters_adapters.py +0 -0
  139. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_reasoning_strip.py +0 -0
  140. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_routing_adaptive.py +0 -0
  141. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_setup_sh.py +0 -0
  142. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_tool_repair.py +0 -0
  143. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_translation_anthropic.py +0 -0
  144. {coderouter_cli-1.9.0 → coderouter_cli-1.10.1}/tests/test_translation_reverse.py +0 -0
@@ -6,6 +6,675 @@ versioning follows [SemVer](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [v1.10.1] — 2026-05-04 (Patch — tool-aware auto routing + Raspberry Pi starter)
10
+
11
+ **Theme: 「ローカル小型モデルでは tool calling できないので tool-laden な request だけクラウドに逃がしたい」というユースケース (OpenClaw + Pi 8GB シナリオ) を declarative に解決。** v1.10.0 で feature complete を宣言した auto_router の 6 matcher を 7 matcher に拡張、`has_tools` を追加して「tools[] を宣言したリクエストか否か」で profile を分岐できるように。併せて Raspberry Pi 8GB 向けの starter YAML (`examples/providers.raspberrypi.yaml`) を同梱、SBC 上で OpenClaw / Claude Code 互換 agent を回すユーザーが yaml 1 個 copy するだけで動く状態にした。
12
+
13
+ 含まれる出荷 2 件:
14
+
15
+ | # | sub-release | テーマ | LOC | tests |
16
+ |---|---|---|---|---|
17
+ | 1 | **has_tools matcher** | `RuleMatcher.has_tools` 7 番目 matcher 追加、OpenAI/Anthropic `tools[]` + OpenAI legacy `functions[]` を一括認識 (OpenClaw + Pi 由来) | ~80 | +7 |
18
+ | 2 | **Raspberry Pi starter** | `examples/providers.raspberrypi.yaml` 新規、Ollama 小型モデル (≤4B) + OpenRouter free 系 + `has_tools` ベースの tool-aware profile 振り分け | YAML のみ | (loader 検証で +0 直接、既存 parametric test に乗る) |
19
+
20
+ - Tests: 871 → **878** (+7、has_tools matcher の 6 シナリオ + `has_tools: false` の "set 扱いだがマッチしない" 安全網テスト)
21
+ - Runtime deps: 5 → 5 (**34 sub-release 連続据え置き**)
22
+ - Backward compat: 完全互換、既存 yaml / API / log payload schema 完全に同じ、新フィールド (`has_tools`) を使わない deployment は挙動完全一致
23
+ - pyproject version: 1.10.0 → 1.10.1
24
+
25
+ ### Migration
26
+
27
+ 不要。**v1.10.0 からの自然なアップグレード**:
28
+
29
+ - `coderouter` コマンド名 / Python import 名 / providers.yaml の format / env 変数 / ingress URL すべて完全に同じ
30
+ - 既存 `auto_router.rules[]` は何も変わらない、`has_tools` matcher を使い始めるには yaml に 1 行足すだけ
31
+ - v1.10.0 で v1.6 系 auto_router を「6 matcher で feature complete」と宣言した直後の追加だが、同じ宣言型 framework の延長線で構造変更なし — 「7 matcher で改めて feature complete」と読み替えて差し支えない
32
+
33
+ ### Out of scope (v1.11 以降)
34
+
35
+ - **Provider capability gate for tools** — `capabilities.tools=false` を fallback chain の skip ゲートとして機能させる案。本 patch は profile レベルで振り分ける方針 (router で chain を切り替える) で `has_tools` matcher を採用、provider レベルの skip ゲートは別 issue。CodeRouter の chain semantics (順次フォールバック + downgrade) の互換性検討が必要なため、必要性が確認できてから着手。
36
+ - **小型ローカルモデルの tool-call repair 強化** — 現状 `tool_repair.py` は `<tool_call>{...}</tool_call>` ラッパ形式の救済を行うが、1-4B モデルが返す自由形式の text からの推測救済は別領域 (`tool_emulation`)。プロンプトテンプレ書き換えで誘導する手段もあり、設計検討は v2.0 後送り。
37
+
38
+ ### Files touched
39
+
40
+ ```
41
+ A examples/providers.raspberrypi.yaml
42
+ M CHANGELOG.md
43
+ M coderouter/config/schemas.py
44
+ M coderouter/routing/auto_router.py
45
+ M pyproject.toml
46
+ M tests/test_auto_router.py
47
+ ```
48
+
49
+ ---
50
+
51
+ ### has_tools matcher (OpenClaw + Raspberry Pi 由来)
52
+
53
+ **Theme: tools[] を宣言したリクエストだけクラウドに振り分け、ローカル小型モデルは tool 不要の素朴な chat に専念させる。** Raspberry Pi 8GB / Jetson Nano クラスの SBC で OpenClaw 等の tool-aware agent を動かしたい時、CPU 推論で実用域に入る Ollama モデル (≤4B) は tool calling が苦手 (`finish_reason: tool_calls` を返さない / 引数 JSON が壊れる / 自由形式 text に bury される) で、結果として agent 側からは「tool 呼び出しが起きてない」状態になる。`auto_router.rules[].if.has_tools` を 7 番目の matcher として追加することで、profile レベルで「tools あり → クラウド (Qwen3-Coder/gpt-oss/Gemini-Flash の OpenRouter free)」「tools 無し → ローカル小型」を declarative に切り替えられる。
54
+
55
+ ユースケース例 (Raspberry Pi 8GB starter `examples/providers.raspberrypi.yaml` から抜粋):
56
+
57
+ ```yaml
58
+ auto_router:
59
+ rules:
60
+ - id: user:has-tools-go-cloud
61
+ profile: with-tools # OpenRouter free 系のみ
62
+ match:
63
+ has_tools: true
64
+ - id: user:image-go-cloud
65
+ profile: vision # Gemini Flash 1M ctx
66
+ match:
67
+ has_image: true
68
+ - id: user:longcontext-go-cloud
69
+ profile: longcontext
70
+ match:
71
+ content_token_count_min: 32000
72
+ default_rule_profile: local-chat # qwen3.5:2b/4b / gemma3:1b ローカル
73
+ ```
74
+
75
+ OpenClaw (毎ターン Bash/Read/Write 等の tool を declare する agent) を `OPENAI_BASE_URL=http://<pi-ip>:8088/v1` で繋ぐと、tool-laden traffic は自動でクラウドに飛び、軽い chat だけが Pi 上のローカルで処理される。OPENROUTER_API_KEY のみ設定すればよく、有料 API は不要 (`ALLOW_PAID=false` がデフォルト)。
76
+
77
+ #### なぜ provider レベルの capability gate ではなく profile レベルなのか
78
+
79
+ `ProviderConfig.capabilities.tools=false` フラグは既存 (v0.x からある) だが、現状は `coderouter doctor` の診断表示と `model-capabilities.yaml` registry の解決に使うだけで、fallback chain における skip ゲートには接続されていない。`thinking` / `cache_control` には `will_degrade` ゲート (capability.py の `provider_supports_*`) があるが、tools には同等の skip 機構がない。これは既存の v0.3-D 「downgrade path」(non-native + tools[] あり → 非ストリーミング + tool_repair) に依存していて、provider が tools を返せなくても adapter エラーは出ず、上流から見ると success (空 tool_calls) で chain が fallthrough せず止まってしまう (= 観測症状: 「tool call されてない」)。
80
+
81
+ provider レベルの skip ゲートを後付けするのは chain semantics に踏み込む変更で互換性検討が要るため、本 patch では **profile レベルの宣言型 lever** に留める方針を採用。chain semantics を変えず、auto_router rule の追加で同じ効果を得られ、かつ既存の 6 matcher と完全に同じ規約 (exactly one + first match wins + fast-fail at load) で導入できる。
82
+
83
+ - Tests: 871 → **878** (+7: OpenAI tools[] / Anthropic tools[] / OpenAI legacy functions[] / no-tools fallthrough / 空リスト fallthrough / has_tools rule が code-fence rule より優先 / `has_tools: false` の "set 扱いだがマッチしない" 安全網)
84
+ - Runtime deps: 5 → 5 (34 sub-release 連続据え置き)
85
+ - Backward compat: 完全互換、既存 `auto_router` rule は何も変わらない、`has_tools` を使わない deployment は挙動完全一致
86
+
87
+ #### Changes
88
+
89
+ - `coderouter/config/schemas.py`:
90
+ - `RuleMatcher` に `has_tools: bool | None = None` を追加、`_MATCHER_FIELDS` tuple に追加 (zero/multiple-fields の "exactly one" バリデータが自動適用)。
91
+ - docstring の Variants セクションに 7 番目として `has_tools` を追記、boolean 形状が `has_image` と同じである理由 (`True` のみ意味を持ち、`False` は "set" 扱いだが `_match_rule` の `is True` チェックでマッチしない安全網) と、provider レベルの `capabilities.tools` flag との違い (前者は profile-level routing、後者は doctor の診断補助で chain skip ゲートではない) を明示。
92
+
93
+ - `coderouter/routing/auto_router.py`:
94
+ - `_has_tools_in_body(body)` ヘルパを新設 — body の top-level `tools[]` (OpenAI Chat Completions / Anthropic Messages API 共通) と `functions[]` (OpenAI legacy、deprecated だが pinned SDK で残存) を一括認識、空リスト / None は False (lazy init 対応)。
95
+ - `_match_rule(rule, message, text, model, estimated_tokens, has_tools)` シグネチャに `has_tools: bool` を追加、`has_tools is True` 分岐を 7 番目として実装。
96
+ - `classify(...)` 内で `_has_tools_in_body(body)` を一度だけ呼んで rule iteration に渡す。`user_msg is None` でも `has_tools` rule は評価する (system-only prompt + tools[] declaration の構成にも対応)。
97
+ - `_emit_resolved` / `_emit_fallthrough` の `signals` payload に `has_tools` を追記、`auto-router-resolved` log で「tools あり判定で routing したか」が dashboard / Prometheus exporter から見える。
98
+
99
+ - `tests/test_auto_router.py` Group 8 (tool-aware routing) を新設、7 ケース:
100
+ - `test_classify_request_with_openai_tools_routes_to_with_tools` — 基本ケース、OpenAI 形式 `tools[].function` → `with-tools` profile。
101
+ - `test_classify_request_with_anthropic_tools_routes_to_with_tools` — Anthropic 形式 `tools[].input_schema` も同じ top-level `tools` キーなので、単一 matcher で両 ingress カバー。
102
+ - `test_classify_request_with_legacy_functions_routes_to_with_tools` — OpenAI legacy `functions[]` (deprecated だが pinned SDK で残存) も tool-laden 扱い。
103
+ - `test_classify_request_without_tools_falls_through` — 逆ケース、tools 宣言なしの plain chat は `default_rule_profile` (Pi の場合は `local-chat`) に落ちる。
104
+ - `test_classify_empty_tools_list_treated_as_no_tools` — `tools: []` / `functions: []` (lazy init shape) は False 扱い、no-spurious-match property を pin。
105
+ - `test_classify_has_tools_first_match_wins_over_later_content_rule` — has_tools rule が code_fence rule より前に置かれた時、両方マッチする body でも先勝、global "first match wins" を新 matcher にも適用。
106
+ - `test_has_tools_false_rejected_at_load` — `has_tools: False` が `_exactly_one` を通過するが `_match_rule` の `is True` チェックでマッチしない安全網を文書化、誤設定時もデフォルト経路に落ちることを保証。
107
+
108
+ #### Files touched
109
+
110
+ ```
111
+ M CHANGELOG.md
112
+ M coderouter/config/schemas.py
113
+ M coderouter/routing/auto_router.py
114
+ M pyproject.toml
115
+ M tests/test_auto_router.py
116
+ ```
117
+
118
+ ---
119
+
120
+ ### Raspberry Pi 8GB starter (`examples/providers.raspberrypi.yaml`)
121
+
122
+ **Theme: SBC で OpenClaw を動かす最小構成を yaml 1 個に集約。** v1.10.1 で追加した `has_tools` matcher を主役にした starter で、`coderouter serve` 1 発で Pi 上のローカル ollama (qwen3.5:2b/4b、qwen2.5:1.5b、gemma3:1b) と OpenRouter free 系 (qwen3-coder:free / gpt-oss-120b:free / gemini-2.5-flash:free) が tool-aware に振り分けられる。OPENROUTER_API_KEY のみ要設定、有料 API キー不要 (`ALLOW_PAID=false` がデフォルト)。
123
+
124
+ #### 設計の要点
125
+
126
+ - **ローカル全部 `tools: false`** — Pi 8GB に乗る ≤4B モデルは tool_calls を安定して返せないため capability で明示的に `false`。これは doctor 診断用の宣言で、実 routing は `has_tools` matcher が profile レベルで振り分けるので二重防御になる。
127
+ - **`num_ctx: 8192` + `num_predict: 1024` 制限** — Pi の CPU 推論は context 縮めた方が prefill が現実的、デフォルト ollama の 2048 だと OpenClaw の system prompt で詰む & 2048 から 32K に上げると prefill が分単位になるので 8K が現実的中間点。
128
+ - **画像 / 長尺 (32K+) もクラウドへ** — Pi では Gemma 4 E4B (vision capable だが 9.6GB で 8GB Pi に乗らない) の代わりに、`has_image` rule で OpenRouter Gemini Flash (1M ctx + vision native) に逃がす。
129
+ - **OpenRouter free 3 モデルで vendor diversity** — qwen-coder / gpt-oss / gemini-flash の 3 ベンダーを並べて、daily cap (~200 req/day per model per account) 当たり時の rate-limit 逃げ場を確保。
130
+ - **`output_filters: [strip_thinking, strip_stop_markers]` を Qwen 系で常時適用** — Pi で動かす Qwen 3.5 系は `<think>...</think>` リーク + `<|im_end|>` 漏れの両方を観測、両方 strip。
131
+
132
+ #### Tests
133
+
134
+ `tests/test_examples_yaml.py::test_example_yaml_loads` が `examples/providers*.yaml` を parametric にカバーしているため、`providers.raspberrypi.yaml` も自動でこの test に乗る。新たに pin したい invariant (例: ローカル全部 `tools: false`、`has_tools` rule の存在、auto_router default が `local-chat` 等) があれば後続 patch で個別 test 追加可能だが、本 patch では parametric の loader-clean property のみ確保。
135
+
136
+ #### Files touched
137
+
138
+ ```
139
+ A examples/providers.raspberrypi.yaml
140
+ ```
141
+
142
+ ---
143
+
144
+ ## [v1.10.0] — 2026-05-01 (Umbrella tag — Cost enforcement + Long-run reliability completion + Auto-router feature complete)
145
+
146
+ **Theme: 「観測 → 理解 → 行動」を 3 軸で完成、Vision pillar P2/P3 が揃う。** v1.9.1 (patch) で取り切った 2 機能 (v1.9-B2 streaming usage 集約 + per-model auto-routing) は事実上 v1.10 backlog の助走、本 v1.10.0 で残り 3 機能を minor として束ねて出荷。CodeRouter は **「Local LLM で agent を長時間回すための信頼性層」** という Vision の v1.x 担当分が完成 — context overflow (L1) と quality drift (L4) を除く 4 系統障害 (L2/L3/L5/L6) を体系的に対処、auto-router の declarative 6 matcher も揃い、cost 系は観測 (v1.9-D) → enforcement (v1.10) で経路が閉じた。
147
+
148
+ 含まれる出荷 5 件 (`docs/inside/future.md §6.6` の v1.10 着手順序、本 release で全完了):
149
+
150
+ | # | sub-release | テーマ | LOC | tests | 出荷先 |
151
+ |---|---|---|---|---|---|
152
+ | 1 | **v1.9-B2** | streaming 経路の usage 集約 (`_StreamUsageAccumulator`、placeholder→観測値) | ~150 | +3 | v1.9.1 |
153
+ | 2 | **per-model auto-routing** | `RuleMatcher.model_pattern` (Opus/Sonnet/Haiku 分岐、free-claude-code 由来) | ~120 | +5 | v1.9.1 |
154
+ | 3 | **provider 月次予算上限** | `BudgetTracker` + `cost.monthly_budget_usd` (LiteLLM 由来 / v1.9-D 累積版) | ~250 | +8 | **v1.10.0** |
155
+ | 4 | **v1.9-E phase 2 (L2/L5)** | Memory pressure detector + Backend health 状態機械 (Vision pillar 完成) | ~370 | +27 | **v1.10.0** |
156
+ | 5 | **longContext auto-switch** | `RuleMatcher.content_token_count_min` (claude-code-router 由来) | ~80 | +5 | **v1.10.0** |
157
+
158
+ - Tests: 838 (v1.9.1) → **871** (+33: 本 minor 単独 +27 from v1.9-E phase 2 + 8 budget + 5 longContext から、v1.10.0 で純増 +33)
159
+ - Runtime deps: 5 → 5 (**33 sub-release 連続据え置き**) — 最初から守ってきた `fastapi / uvicorn / httpx / pydantic / pyyaml` のみ
160
+ - pyproject version: 1.9.1 → 1.10.0
161
+
162
+ ### Pillar 別の達成
163
+
164
+ #### P2 Long-run Reliability (v1.9-E 系) — Vision の核心が揃う
165
+
166
+ 6 系統障害体系 (`docs/inside/future.md §1`) のうち v1.x で取りに行くと宣言した分が完成:
167
+
168
+ | # | 障害 | v1.x 担当 | 状態 |
169
+ |---|---|---|---|
170
+ | **L1** | Context overflow | (v2.0-F) | ⏳ |
171
+ | **L2** | Memory pressure | v1.9-E phase 2 | ✅ v1.10.0 |
172
+ | **L3** | Tool loop | v1.9-E phase 1 | ✅ v1.9.0 |
173
+ | **L4** | Quality drift | (v2.0-G) | ⏳ |
174
+ | **L5** | Backend crash / health | v1.9-E phase 2 | ✅ v1.10.0 |
175
+ | **L6** | Mid-stream interrupt | v0.3-A 既存 + (v2.0-H 強化) | ✅ baseline |
176
+
177
+ L2/L3/L5 の 3 兄弟が `coderouter/guards/` 配下に並び、それぞれ `MemoryPressureGuard` / `_apply_tool_loop_guard` / `BackendHealthMonitor` が pure module として独立。engine 統合は `_observe_provider_failure` / `_observe_provider_success` の 2 chokepoint で済む綺麗な設計に着地。
178
+
179
+ #### Cost pillar (v1.9-D 系) — 観測 → 制約の経路が閉じる
180
+
181
+ | 段階 | sub-release | 役割 |
182
+ |---|---|---|
183
+ | **観測** | v1.9-A | `cache-observed` log + cache hit/miss 4-class outcome |
184
+ | **観測のカバレッジ** | v1.9-B2 (v1.9.1) | streaming 経路まで完全カバー、placeholder ゼロ化 |
185
+ | **理解** | v1.9-D | per-provider USD cost + cache savings 別計算 (LiteLLM 既存品が落とす精度) |
186
+ | **制約** | **v1.10.0** | `monthly_budget_usd` で per-provider 月次 cap、UTC 暦月 in-memory bucketing |
187
+
188
+ v1.9.0 GA 時点で「観測の 4-class 精度」「LiteLLM 既存品より精度高い cost 計算」が CodeRouter の差別化軸として確立、v1.10.0 でそれを enforcement に活用できる経路が閉じた。
189
+
190
+ #### Auto-router (v1.6 系) — 6 matcher で feature complete
191
+
192
+ | # | matcher | 由来 | 出荷 |
193
+ |---|---|---|---|
194
+ | 1 | `has_image` | v1.6-A bundled | v1.6.0 |
195
+ | 2 | `code_fence_ratio_min` | v1.6-A bundled | v1.6.0 |
196
+ | 3 | `content_contains` | v1.6-A user-defined | v1.6.0 |
197
+ | 4 | `content_regex` | v1.6-A user-defined | v1.6.0 |
198
+ | 5 | `model_pattern` | free-claude-code 由来 | v1.9.1 |
199
+ | 6 | `content_token_count_min` | claude-code-router 由来 | **v1.10.0** |
200
+
201
+ declarative routing が「latest message の content / 画像 (per-turn signal) + request 全体の model id / token count (request-shape signal)」で完備、competitive analysis で抽出した v1.10 候補の取り込みは打ち止め、これ以降の追加は要望ドリブンで再開する想定。
202
+
203
+ ### Migration
204
+
205
+ 不要。**v1.9.1 / v1.9.0 / v1.9.0a* からの自然なアップグレード**:
206
+
207
+ - `coderouter` コマンド名 / Python import 名 / providers.yaml の format / env 変数 / ingress URL すべて完全に同じ
208
+ - 新しい schema field (`cost.monthly_budget_usd` / `memory_pressure_*` / `backend_health_*` / `content_token_count_min`) は全部 optional + 安全側 default (`monthly_budget_usd: None` / action は `warn` か `off`)、未指定 deployment は v1.9.x と挙動完全一致
209
+ - 新しい log event (`skip-budget-exceeded` / `chain-budget-exceeded` / `memory-pressure-detected` / `skip-memory-pressure` / `chain-memory-pressure-blocked` / `backend-health-changed` / `demote-unhealthy-provider`) は既存 `cache-observed` / `provider-failed` / etc. と同じ JSON 形式、外部 consumer は受信側に dispatch 追加するだけで対応可能
210
+
211
+ ### Out of scope (v2.0 以降)
212
+
213
+ - **L1 Context overflow** → v2.0-F (semantic compression、context budget per-mode)
214
+ - **L4 Quality drift detection** → v2.0-G (response 品質 rolling window 観測)
215
+ - **L6 Mid-stream stitching 強化** → v2.0-H (resumable continuation)
216
+ - **Continuous probing** → v2.0-I (毎時/毎日 model ヘルスチェック、HF dataset 公開)
217
+ - **Persistent budget state** (sqlite / Redis) — 5-deps 不変原則で v1.x 範囲では却下
218
+ - **L5 active probing** (60s 間隔の能動 GET /api/version) — v2.0-I の領域、passive で 80% カバーできているため後回し
219
+ - **tiktoken / SentencePiece による正確なトークンカウント** — 5-deps 不変原則で却下
220
+
221
+ 詳細は `docs/inside/future.md §7` を参照。
222
+
223
+ ### Files touched
224
+
225
+ ```
226
+ A coderouter/guards/backend_health.py
227
+ A coderouter/guards/memory_pressure.py
228
+ A coderouter/routing/budget.py
229
+ A tests/test_backend_health.py
230
+ A tests/test_budget.py
231
+ A tests/test_memory_pressure.py
232
+ M CHANGELOG.md
233
+ M coderouter/config/schemas.py
234
+ M coderouter/logging.py
235
+ M coderouter/metrics/collector.py
236
+ M coderouter/metrics/prometheus.py
237
+ M coderouter/routing/auto_router.py
238
+ M coderouter/routing/fallback.py
239
+ M docs/inside/future.md
240
+ M plan.md
241
+ M pyproject.toml
242
+ M tests/test_auto_router.py
243
+ ```
244
+
245
+ ---
246
+
247
+ ### v1.10 候補 #5: longContext auto-switch (claude-code-router 由来)
248
+
249
+ **Theme: コンテキスト窓圧迫の自動逃がし。** 長文プロンプト (会話ヒストリーの累積、コードベース貼り付け等) が来た時、context window の小さいモデル (200K Anthropic) ではなく 1M ctx の Gemini Flash 系へ自動切替する仕組み。`auto_router.rules[].if.content_token_count_min` を 6 番目の matcher として追加、既存 5 種と同じ "exactly one" 規約を継承。
250
+
251
+ ユースケース例:
252
+
253
+ ```yaml
254
+ auto_router:
255
+ rules:
256
+ - if: { content_token_count_min: 32000 }
257
+ route_to: longcontext
258
+ default_rule_profile: writing
259
+
260
+ profiles:
261
+ - name: longcontext
262
+ providers:
263
+ - openrouter-gemini-flash-free # 1M ctx
264
+ - anthropic-haiku-direct # 200K ctx
265
+ - name: writing
266
+ providers: [anthropic-sonnet-direct]
267
+ ```
268
+
269
+ agent が短いやり取りを 100 ターン続けて context が膨らんだ時、自動で 1M ctx チェーンに切り替わる。`free-claude-code` / `claude-code-router` 由来のニーズを CodeRouter の declarative auto_router framework に取り込んだ形。
270
+
271
+ #### 設計判断: char/4 ヒューリスティック vs tiktoken
272
+
273
+ token カウントは `len(text) // 4` の素朴ヒューリスティック (OpenAI 公式の rule of thumb)。**5-deps 不変原則** (`plan.md §5.4`) を守るため tiktoken / SentencePiece は導入しない。トレードオフ:
274
+
275
+ - **English 散文 / コード**: char/4 はやや緩い (実際は ~3.5/token)、`min` 比較なので大きい threshold で安全側に倒せる
276
+ - **CJK (日本語/中国語/韓国語)**: char/4 は **保守的にカウント不足** (実際は ~1.5-2 char/token)、つまり 100k 文字の日本語プロンプトを ~25k tokens と過小評価。これは積極的に context overflow を引き起こす方向ではないので fail-safe な誤差
277
+ - **トレードオフ判断**: tiktoken なら正確だが 100MB 級の依存追加、SentencePiece でも 50MB 級。CodeRouter は「個人開発者用の signal-based router」なので、operator が threshold を実機運用フィードバックで調整する前提のヒューリスティックで十分
278
+
279
+ #### Other matchers との違い
280
+
281
+ `content_contains` / `content_regex` / `has_image` は **latest user message** に対して評価 (per-turn signal)、`content_token_count_min` は **request 全体 (system + 全 messages)** を walk して合算 (request-shape signal)。context-window pressure はリクエスト全体の性質なので latest-only では誤検出する。
282
+
283
+ - Tests: 866 → **871** (+5: long-prompt match / 短文 fallthrough / 全 messages walk / 負値 reject / first-match-wins precedence)
284
+ - Runtime deps: 5 → 5 (33 sub-release 連続据え置き)
285
+ - Backward compat: 完全互換、既存 `auto_router` rule は何も変わらない
286
+
287
+ #### Changes
288
+
289
+ - `coderouter/config/schemas.py`:
290
+ - `RuleMatcher` に `content_token_count_min: int | None = None` (`ge=1`) を追加、`_MATCHER_FIELDS` に登録 (既存の "exactly one" バリデータが自動適用、`ge=1` で 0/負値は schema load で reject)。
291
+ - docstring の Variants セクションに 6 番目として明記、char/4 ヒューリスティック + 全 messages 対象 (latest-only の他 matcher と差別化) + 5-deps トレードオフを文書化。
292
+
293
+ - `coderouter/routing/auto_router.py`:
294
+ - `_estimate_total_tokens(body)` ヘルパを新設 — `body["system"]` (str / list-of-blocks 両対応) と `body["messages"]` の全 message を walk、`_extract_text` で text を抽出、char 合算を `_CHARS_PER_TOKEN_HEURISTIC=4` で除して token 推定。image / non-text blocks は 0 寄与。
295
+ - `_match_rule` に `estimated_tokens: int` パラメータを追加、6 番目の分岐として `content_token_count_min` 比較を実装。
296
+ - `classify(...)` 内で `_estimate_total_tokens(body)` を 1 回計算、ルール評価ループに流す。`_emit_resolved` / `_emit_fallthrough` の signals payload に `estimated_tokens` を追記、dashboard / Prometheus exporter から「何トークン推定でその profile に流れたか」が見える。
297
+
298
+ - `tests/test_auto_router.py` Group 7 を新設、5 ケース:
299
+ - `test_classify_long_prompt_routes_to_longcontext` — 200,000 chars (~50,000 tokens) → 32,000 threshold を超えて longcontext profile。
300
+ - `test_classify_short_prompt_below_threshold_falls_through` — 1,000 chars (~250 tokens) → default_rule_profile (writing) に落ちる。
301
+ - `test_classify_long_context_walks_all_messages_not_just_latest` — 長い会話 history + 短い最新ユーザー msg、latest-only matcher なら拾えないケースを longContext は拾うことを pin。
302
+ - `test_content_token_count_min_rejects_non_positive_at_load` — `0` / `-5` を `RuleMatcher` 構築時に reject (pydantic `ge=1`)。
303
+ - `test_long_context_first_match_wins_over_later_image_rule` — token-count rule を先に置けば長文+画像 body でも longcontext が勝つ、先勝順序を pin。
304
+
305
+ #### Files touched
306
+
307
+ ```
308
+ M CHANGELOG.md
309
+ M coderouter/config/schemas.py
310
+ M coderouter/routing/auto_router.py
311
+ M tests/test_auto_router.py
312
+ ```
313
+
314
+ #### Why now
315
+
316
+ `docs/inside/future.md §6.6` の v1.10 着手順序 **#5 (最終)**。実装規模 ~80 LOC + tests ~150 LOC、半日工数 (見積 ~150-200 LOC / 3-5 日より大幅短縮 — auto_router の matcher 追加パターンが per-model auto-routing で確立済み、全 messages walk 用の `_estimate_total_tokens` ヘルパだけ新設で済んだ)。
317
+
318
+ これで **v1.10 候補 5 件全完了** (#1 v1.9-B2 / #2 per-model auto-routing は v1.9.1、#3 monthly budget / #4 v1.9-E phase 2 / #5 longContext auto-switch は本 [Unreleased] umbrella)。次回 PyPI publish 時に **v1.10.0 minor として umbrella tag 化**できる位置 (Vision pillar 完成 + auto-router 全 6 matcher 揃 + cost enforcement 完成)。
319
+
320
+ #### Out of scope (v2.0 以降 / 将来の精緻化)
321
+
322
+ - **tiktoken / SentencePiece による正確なトークンカウント** — 5-deps 不変原則で却下。実機運用で threshold tuning が困難になったら再検討。
323
+ - **Provider 別 context-window 自動推測** — `model-capabilities.yaml` に `max_context_tokens` を加えれば自動推測できる方向もあるが、operator の運用シナリオ次第なので明示宣言で十分。
324
+ - **動的 threshold (chain の最小 max_context_tokens に応じて)** — 同上、明示宣言で十分。
325
+
326
+ ---
327
+
328
+ ### v1.10 候補 #4: v1.9-E phase 2 (L2 memory pressure + L5 backend health) — Vision 完成
329
+
330
+ **Theme: 「8 時間 agent ループでも止まらない」を約束する Long-run Reliability pillar (P2) を完成させる。** v1.9.0 で L3 (tool-loop guard) を phase 1 として先行出荷したが、Vision で謳う 6 系統障害体系のうち **L2 (Memory pressure)** と **L5 (Backend crash / health)** が phase 2 として残っていた。本 release で両方を opt-in guard として実装、`coderouter/guards/` 配下に並ぶ 3 兄弟 (tool_loop / memory_pressure / backend_health) で長時間運用の中核 3 障害をカバーする。
331
+
332
+ #### L2: Memory pressure detection + cooldown
333
+
334
+ ローカル backend (Ollama / LM Studio / llama.cpp) が VRAM 枯渇で 5xx を返す時、エラー本文に `out of memory` / `CUDA out of memory` / `insufficient memory` / `model requires more system memory` 等の OOM フレーズが入る。L2 はこれを観察して当該 provider をクールダウンに入れ、次の chain resolve から `memory_pressure_cooldown_s` 秒間 skip する:
335
+
336
+ ```yaml
337
+ profiles:
338
+ - name: default
339
+ providers: [ollama-large, ollama-small, openrouter-fallback]
340
+ memory_pressure_action: skip # off / warn / skip (default: warn)
341
+ memory_pressure_cooldown_s: 120 # default 120s, 10〜3600 s
342
+ ```
343
+
344
+ `action=skip` の時、ollama-large が OOM → 120 秒間 ollama-large を chain から除外、ollama-small や openrouter-fallback に流れる → クールダウン明けで再度 ollama-large を試す。`action=warn` (default) は log のみ、`off` は完全に無効化 (zero overhead)。
345
+
346
+ #### L5: Backend health (consecutive failure state machine)
347
+
348
+ backend が突発 crash した時の defacto demote。`BackendHealthMonitor` が provider ごとに consecutive failure 数を数え、`backend_health_threshold` (default 3) で `HEALTHY → DEGRADED`、`2 x threshold` で `DEGRADED → UNHEALTHY` に遷移。1 回の成功で即 HEALTHY 復帰。`backend_health_action: demote` の時、UNHEALTHY な provider は chain 末尾に降格 (skip ではなく **demote** — 死活確認の 1 リクエストは飛ばす、best-effort principle):
349
+
350
+ ```yaml
351
+ profiles:
352
+ - name: default
353
+ providers: [ollama-local, anthropic-fallback]
354
+ backend_health_action: demote # off / warn / demote (default: warn)
355
+ backend_health_threshold: 3
356
+ ```
357
+
358
+ v1.9-C の `adaptive` (rolling-window 連続観測 + debounce) とは直交関係 — adaptive が「徐々に遅くなった」勾配ケース、L5 が「突発 crash」二値ケース。両者重ね掛け可能で、両方 enable した chain では「latency 劣化 → adaptive demote」「crash → L5 demote」の両方の信号で並べ替え。
359
+
360
+ #### Numbers
361
+
362
+ - Tests: 839 → **866** (+27 累積、L2 +19 / L5 +8)
363
+ - Runtime deps: 5 → 5 (32 sub-release 連続据え置き)
364
+ - Backward compat: 完全互換、両 `*_action` のデフォルトは `warn` (= log のみ、行動変化なし)。`off` で完全無効化。既存 v1.9.x deployment は yaml 変更なしで自然継続
365
+
366
+ #### Changes
367
+
368
+ - `coderouter/guards/memory_pressure.py` 新設 (~170 LOC):
369
+ - `is_memory_pressure_error(exc)` — 純関数、9 種の OOM フレーズに対する case-insensitive substring match (Ollama / LM Studio / llama.cpp / 汎用 CUDA / Metal の実観測パターン)。
370
+ - `MemoryPressureGuard` — per-provider TTL cooldown tracker、`mark_pressured` / `is_pressured` / `pressured_until` API、`time.monotonic` ベースで wall-clock skew 耐性、tests は `now=` 引数で deterministic 注入。
371
+
372
+ - `coderouter/guards/backend_health.py` 新設 (~200 LOC):
373
+ - `BackendHealthMonitor` — per-provider 状態機械 (HEALTHY / DEGRADED / UNHEALTHY)、`record_attempt(success, threshold)` で観測、状態遷移時のみ `HealthTransition` を返す (no log spam on stable state)、threshold は per-call で profile 違いに対応。
374
+ - 状態機械の遷移ルール: 失敗 N 回 (= threshold) で DEGRADED、2N 回で UNHEALTHY、1 回成功で即 HEALTHY 復帰。
375
+
376
+ - `coderouter/config/schemas.py`:
377
+ - `FallbackChain` に `memory_pressure_action` / `memory_pressure_cooldown_s` (L2) と `backend_health_action` / `backend_health_threshold` (L5) を追加。L3 (`tool_loop_*`) と同じ命名 + 同じ "off / warn / 行動" tri-state パターン。
378
+ - 各 field に `Literal` 型 + range 制約 + 詳細 docstring (どの障害を見るか、L2/L5 の使い分け、v1.9-C adaptive との関係)。
379
+
380
+ - `coderouter/logging.py`:
381
+ - L2: `log_memory_pressure_detected` / `log_skip_memory_pressure` / `log_chain_memory_pressure_blocked` ヘルパ + 3 つの TypedDict payload。paid-gate / budget-gate と完全 symmetric。
382
+ - L5: `log_backend_health_changed` (state transition、payload に old_state/new_state/consecutive_failures) / `log_demote_unhealthy_provider` ヘルパ + 2 つの TypedDict。
383
+
384
+ - `coderouter/routing/fallback.py`:
385
+ - `FallbackEngine` に `_memory_pressure` / `_backend_health` lazy property を追加 (`_adaptive` / `_budget` と同じパターン、`__new__` 経由 legacy tests 対応)。
386
+ - `_observe_provider_failure(provider, exc, profile)` ヘルパ — L2 OOM 検出 + L5 失敗カウンタを single chokepoint で dispatch、6 つの failure site (4 entry point × non-stream/mid-stream) から呼ぶ。
387
+ - `_observe_provider_success(provider, profile)` 新設 — L5 状態機械の成功遷移を 4 success site (provider-ok 時) から呼ぶ。
388
+ - `_resolve_chain` を 4-pass に拡張: paid → budget → **L2 pressure skip** → L5 demote。L2 は filter (skip)、L5 は reorder (demote)、両者の役割分担を明確化。L5 demote は `unhealthy and healthy` 両方ある時のみ実施 (uniformly UNHEALTHY chain は no-op、log spam 抑制)。
389
+
390
+ - `coderouter/metrics/collector.py`:
391
+ - `_provider_skipped_memory_pressure: Counter` + `_chain_memory_pressure_blocked_total: int` (L2)。
392
+ - `_provider_demoted_unhealthy: Counter` + `_backend_health_transitions: dict[str, Counter]` (L5、transition を destination state でキー)。
393
+ - `skip-memory-pressure` / `chain-memory-pressure-blocked` / `backend-health-changed` / `demote-unhealthy-provider` event の dispatch + snapshot/reset 配線。
394
+
395
+ - `coderouter/metrics/prometheus.py`:
396
+ - `coderouter_provider_skipped_total{reason="memory_pressure"}` を既存 `paid` / `unknown` / `budget` と同 counter に同居。
397
+ - `coderouter_provider_demoted_unhealthy_total{provider}` (L5)、`coderouter_backend_health_transitions_total{provider, state}` (L5)、`coderouter_chain_memory_pressure_blocked_total` (L2) を追加。
398
+
399
+ - `tests/test_memory_pressure.py` 新設 (~360 LOC、+19 tests):
400
+ - **Group 1 (detector)**: 8 種の OOM フレーズを parameterize で網羅、5 種の非 OOM 失敗で false 確認。
401
+ - **Group 2 (guard)**: TTL cooldown / lazy expiry / re-mark 拡張。
402
+ - **Group 3 (engine)**: action=warn は log only / action=skip は cooldown 中 chain skip + fallback / action=off は完全無効 / 全 provider pressured で `chain-memory-pressure-blocked` warn + `NoProvidersAvailableError`。
403
+
404
+ - `tests/test_backend_health.py` 新設 (~340 LOC、+8 tests):
405
+ - **Group 1 (monitor)**: 初期状態 HEALTHY、threshold/2x threshold での状態遷移、success で UNHEALTHY → HEALTHY 即復帰、stable state で transition 返さない。
406
+ - **Group 2 (engine action)**: warn は log only / demote で chain reorder (try-provider 順序検証) / off で監視ゼロ / UNHEALTHY → HEALTHY recovery transition log。
407
+ - **Group 3 (chain reorder)**: 全 provider UNHEALTHY 時は demote no-op (log spam なし、best-effort 続行)。
408
+
409
+ #### Files touched
410
+
411
+ ```
412
+ A coderouter/guards/backend_health.py
413
+ A coderouter/guards/memory_pressure.py
414
+ A tests/test_backend_health.py
415
+ A tests/test_memory_pressure.py
416
+ M CHANGELOG.md
417
+ M coderouter/config/schemas.py
418
+ M coderouter/logging.py
419
+ M coderouter/metrics/collector.py
420
+ M coderouter/metrics/prometheus.py
421
+ M coderouter/routing/fallback.py
422
+ ```
423
+
424
+ #### Why now
425
+
426
+ `docs/inside/future.md §6.6` v1.10 着手順序 #4 — **Vision の核心**。v1.9.0 GA で「v1.10 候補」と整理した backlog で唯一 ~900 LOC スケールの Vision-critical pillar。v1.9.1 の monthly-budget で cost 軸の運用が見えるようになった上に、L2/L5 で **「6 系統障害のうち L2/L3/L5 を体系的に対処」** が完成。`L1 Context overflow` / `L4 Quality drift` / `L6 Mid-stream interrupt 強化` は v2.0-F/G/H の領域、v1.x で cover する long-run reliability の到達点として位置付け。
427
+
428
+ #### Out of scope (v2.0 以降)
429
+
430
+ - **L5 active probing** (60s 間隔の能動 GET /api/version) — 受動 observation で十分カバーできる範囲、active probe を加えると httpx の lifecycle / mocking の複雑度が増えるため v2.0-I (`continuous probing` pillar 拡張) で再検討。
431
+ - **L2 thresholding (count of OOM events before mark)** — single OOM = mark の素朴実装で十分。複数 OOM 観測でしか mark しないという調整は実機運用 feedback が来てから。
432
+ - **HEALTHY/DEGRADED/UNHEALTHY の 4 段階以上化** — 3 段階で十分、運用 feedback が来てから検討。
433
+
434
+ ---
435
+
436
+ ### v1.10 候補 #3: provider 月次予算上限 (LiteLLM 由来 / v1.9-D の累積版)
437
+
438
+ **Theme: v1.9-D で「いくら使ったか」が見えるようになった所に、「これ以上使うな」を宣言できる gate を足す。** v1.9-D の `cost_total_usd` は process-lifetime cumulative なので billing-cycle 上限としては使えない (再起動で消える + 月境界で reset しない)。本機能は **per-provider monthly USD cap** を `cost.monthly_budget_usd` で宣言できるようにし、UTC 暦月単位の running total が cap に達した provider を chain resolver が skip するようにする。
439
+
440
+ ユースケース例:
441
+
442
+ ```yaml
443
+ providers:
444
+ - name: anthropic-direct
445
+ kind: anthropic
446
+ base_url: https://api.anthropic.com
447
+ model: claude-sonnet-4-6
448
+ cost:
449
+ input_tokens_per_million: 3.0
450
+ output_tokens_per_million: 15.0
451
+ monthly_budget_usd: 5.0 # ← v1.10 新フィールド
452
+ - name: ollama-local
453
+ base_url: http://localhost:11434/v1
454
+ model: qwen3.6:35b-a3b
455
+ # 無料 / cost 未設定 = 無制限 (skip 対象外)
456
+ profiles:
457
+ - name: default
458
+ providers: [anthropic-direct, ollama-local] # paid → free fallback
459
+ ```
460
+
461
+ `anthropic-direct` が今月 5 USD 消費した時点で chain resolver が skip し、`ollama-local` (無料) に fall through する。`skip-budget-exceeded` info + (全 provider が cap に達した時のみ) `chain-budget-exceeded` warn が emit される。
462
+
463
+ **Persistence の意図的な制限**: in-memory only。プロセス再起動で running total が 0 にリセットされる。**5-deps 不変原則** (`plan.md §5.4`) を守るため sqlite / Redis / disk は導入しない。durable な月次 enforcement が必要なオペレータは v1.9-D の `cost_total_usd` panel を外部監視ツール (Prometheus alertmanager / Grafana threshold) で受ければ十分カバー可能。
464
+
465
+ - Tests: 831 → **839** (+8: BudgetTracker pure 3 / CostConfig schema 2 / engine integration 3)
466
+ - Runtime deps: 5 → 5 (31 sub-release 連続据え置き)
467
+ - Backward compat: 完全互換、`monthly_budget_usd` 未設定 deployment は挙動完全一致 (opt-in feature)
468
+
469
+ #### Changes
470
+
471
+ - `coderouter/config/schemas.py`:
472
+ - `CostConfig` に `monthly_budget_usd: float | None = None` を追加 (`ge=0.0`、None = 無制限)。
473
+ - docstring で UTC calendar-month + in-memory only persistence を明示、5-deps 不変原則との整合 (no sqlite/Redis) を文書化。
474
+
475
+ - `coderouter/routing/budget.py` 新設 (~190 LOC):
476
+ - `BudgetTracker` クラス — per-provider current-month USD running total を `dict[str, float]` で保持、`threading.RLock` 配下。月境界判定は `_utc_month_key` ヘルパ (UTC `datetime.now()` 経由、tests は `now=` 引数で deterministic に注入可能)。
477
+ - 公開 API: `record(provider, cost_usd)` / `is_over_budget(provider, budget_usd)` / `current_month()` / `total_for_provider(provider)` / `reset()`。
478
+ - **Lazy month rollover**: 各 public call の入口で `_roll_if_needed` を呼び、cached month と current UTC month が違えば `_totals` を clear してから query に答える。background timer 不要。
479
+ - `is_over_budget` は `>=` 比較 — exact-hit の "5.00 USD" は exhausted と判定 (conservative: 次の call は bill しない)。
480
+
481
+ - `coderouter/logging.py`:
482
+ - `SkipBudgetExceededPayload` / `ChainBudgetExceededPayload` TypedDict + `log_skip_budget_exceeded` / `log_chain_budget_exceeded` ヘルパを追加。`log_chain_paid_gate_blocked` のパターンを完全 mirror、payload に `month` (YYYY-MM UTC bucket) を含める。
483
+
484
+ - `coderouter/routing/fallback.py`:
485
+ - `FallbackEngine.__init__` に `_budget_tracker: BudgetTracker = BudgetTracker()` を追加、`_adaptive` と同じ lazy property pattern で `_budget` を露出 (legacy tests の `__new__` 経路でも空 tracker が返る)。
486
+ - `_resolve_chain` を 2-pass に refactor: pass 1 が paid-gate (既存ロジック)、pass 2 が **budget-gate** (新規)。budget-gate は `provider_cfg.cost.monthly_budget_usd` が set されている provider のみ check、`is_over_budget` ならば `skip-budget-exceeded` info を emit して候補から除外。chain が空になった時の aggregate warn は `blocked_by_budget` を優先 (paid-gate より後段で filter したため)、`chain-budget-exceeded` を fire。
487
+ - `_emit_cache_observed` / `_emit_cache_observed_streaming` に `budget: BudgetTracker | None = None` 引数を追加、`compute_cost_for_attempt` の結果が positive な時に `budget.record(provider, cost.total_usd)` を呼ぶ。engine 側 2 callsite (`generate_anthropic` / `stream_anthropic`) で `budget=self._budget` を渡す配線。
488
+
489
+ - `coderouter/metrics/collector.py`:
490
+ - `_provider_skipped_budget: Counter[str]` + `_chain_budget_exceeded_total: int` を追加、`_provider_skipped_paid` / `_chain_paid_gate_blocked_total` の対称配置。
491
+ - `_dispatch` に `skip-budget-exceeded` / `chain-budget-exceeded` event の handler を追加。`reset()` / `snapshot()` も両 counter を含むように拡張。
492
+ - module docstring の event inventory に v1.10 行 2 件追記。
493
+
494
+ - `coderouter/metrics/prometheus.py`:
495
+ - `coderouter_provider_skipped_total{provider, reason="budget"}` を既存の `paid` / `unknown` と同じ counter に同居 (dashboards が reason 別 stack できるように)。
496
+ - `coderouter_chain_budget_exceeded_total` scalar counter を新設 (`coderouter_chain_paid_gate_blocked_total` の対称配置)。
497
+
498
+ - `tests/test_budget.py` 新設 (~340 LOC、+8 tests):
499
+ - **Group 1 (BudgetTracker pure)**: record 蓄積 / is_over_budget の `>=` boundary semantics / 月境界 rollover (`now=` 引数で April→May 跨ぎを deterministic に検証)。
500
+ - **Group 2 (CostConfig schema)**: `monthly_budget_usd: 5.0` 受理、負値 reject (pydantic `ge=0.0`)。
501
+ - **Group 3 (engine integration)**: pre-loaded budget でも primary skip + fallback 経由 (warn なし) / 全 provider cap で `NoProvidersAvailableError` + `chain-budget-exceeded` warn 1 回 / 実 attempt の cost が `BudgetTracker` に蓄積されて 3 回目で skip されることを確認 (real wiring の end-to-end test)。
502
+
503
+ #### Files touched
504
+
505
+ ```
506
+ A coderouter/routing/budget.py
507
+ A tests/test_budget.py
508
+ M CHANGELOG.md
509
+ M coderouter/config/schemas.py
510
+ M coderouter/logging.py
511
+ M coderouter/metrics/collector.py
512
+ M coderouter/metrics/prometheus.py
513
+ M coderouter/routing/fallback.py
514
+ ```
515
+
516
+ #### Why now
517
+
518
+ `docs/inside/future.md §6.6` の v1.10 着手順序 #3。v1.9-D で観測の基盤ができた直後に **enforcement** を足す自然な順序、cost-aware ユーザー (paid backend を組み込む operator) にとって最も価値の高い v1.10 候補。LiteLLM が同等機能を `litellm[proxy]` の中で重実装 (Redis 必須) しているのに対し、CodeRouter は in-memory + 5-deps 維持で「個人開発者用の budget guard」として割り切ることで構造的負債を避ける。
519
+
520
+ #### Out of scope
521
+
522
+ - **Persistent budget state** (sqlite / Redis / disk-backed) — 5-deps 不変原則により未対応。durable enforcement 必要なケースは v1.9-D dashboard を外部 alerting に繋ぐ運用で代替。
523
+ - **Rolling 30-day window** — UTC calendar month で十分 (typical billing cycle と一致、月境界判定の rollover 実装が単純)。rolling window は `_utc_month_key` を date-windowed key に差し替えれば追加できるが、operator request が来てから判断。
524
+ - **Per-profile budget** (vs per-provider) — provider 単位で十分。同じ provider を複数 profile が共有する場合 budget は共有されるべき (実コストの帰属先は provider なので) という意味的にも provider 帰属が正しい。
525
+
526
+ ---
527
+
528
+ ## [v1.9.1] — 2026-05-01 (Patch — v1.10 候補から quick win 2 件先行刈取り)
529
+
530
+ **Theme: v1.9.0 GA で「v1.10 候補」と整理した backlog のうち、構造的負債を伴わない quick win 2 件 (streaming cache 観測の完成形 + agent-driven model 識別子で profile 分岐) を patch として束ねる。** 観測ループの埋め残しと、Claude Code / Cursor 等 agent 側の設定 (Opus / Sonnet / Haiku 使い分け) が CodeRouter の declarative routing に反映できる経路を、完全互換で追加。両機能とも v1.9.0 既存 framework (`cache-observed` log / `auto_router.rules`) の延長線で、新 framework / 依存追加なし。
531
+
532
+ 含まれる出荷 2 件 (`docs/inside/future.md §6.6` の v1.10 着手順序 #1, #2):
533
+
534
+ | # | sub-release | テーマ | LOC | tests |
535
+ |---|---|---|---|---|
536
+ | 1 | **v1.9-B2** | streaming 経路の usage 集約 — `_StreamUsageAccumulator` + `_emit_cache_observed_streaming` で `outcome=unknown` placeholder を観測値に置換 | ~150 | +3 |
537
+ | 2 | **per-model auto-routing** | `RuleMatcher.model_pattern` 5 番目 matcher 追加、`re.fullmatch` で body model id を評価 (free-claude-code 由来) | ~120 | +5 |
538
+
539
+ - Tests: 830 → **838** (+8 累積、v1.9-B2 +3 / per-model +5)
540
+ - Runtime deps: 5 → 5 (30 sub-release 連続据え置き)
541
+ - Backward compat: 完全互換、既存 yaml / API / log payload 全部既存と同じ schema、新フィールド (`model_pattern`) を使わない deployment は挙動完全一致
542
+ - pyproject version: 1.9.0 → 1.9.1
543
+
544
+ ### Migration
545
+
546
+ 不要。**v1.9.0 / v1.9.0a* からの自然なアップグレード**:
547
+
548
+ - `coderouter` コマンド名 / Python import 名 / providers.yaml の format / env 変数 / ingress URL すべて完全に同じ
549
+ - streaming で `cache-observed` log を読んでいる外部 consumer (例: dashboard / Prometheus / 自前 JSONL parser) には、v1.9.0a6 までゼロ固定だった `cache_read_input_tokens` / `cache_creation_input_tokens` / `input_tokens` / `output_tokens` / `outcome` / `cost_usd` / `cost_savings_usd` が観測値に置き換わる。consumer 側は **値が増えた** だけで schema は同じ、ロジック変更不要
550
+ - `auto_router.rules[].if.model_pattern` を使い始めるには yaml に 1 行足すだけ、既存 rule に影響なし
551
+
552
+ ### Out of scope (v1.10 / v1.9.x 続編)
553
+
554
+ [v1.9.0] GA ノートと `docs/inside/future.md §6.6` で示した v1.10 候補から残り 3 件:
555
+
556
+ - **provider 月次予算上限** (LiteLLM 由来、v1.9-D の累積版) — `monthly_budget_usd` で provider 単位の running total + 超過時 skip + log。~400 LOC、3-5 日。
557
+ - **v1.9-E phase 2** — L2 Memory pressure (LM Studio / ollama backend OOM 検知) / L5 Backend health (continuous probe + chain reorder)。**Vision の核心 (8 時間 agent ループでも止まらない)** を完成させる pillar。~900 LOC、1-2 週間。
558
+ - **longContext auto-switch** — `auto_router` rule type 5 として `content_token_count_min` matcher 追加 (claude-code-router task-based 取込)。~200 LOC、3-5 日。
559
+
560
+ これら 3 件は構造拡張を伴うため v1.9.1 patch ではなく v1.10.0 minor で個別 sub-release にして出荷する想定。
561
+
562
+ ### Files touched
563
+
564
+ ```
565
+ M CHANGELOG.md
566
+ M coderouter/config/schemas.py
567
+ M coderouter/routing/auto_router.py
568
+ M coderouter/routing/fallback.py
569
+ M docs/inside/future.md
570
+ M plan.md
571
+ M pyproject.toml
572
+ M tests/test_auto_router.py
573
+ M tests/test_fallback_cache_observed.py
574
+ ```
575
+
576
+ ---
577
+
578
+ ### per-model auto-routing (v1.10 候補 #2、free-claude-code 由来)
579
+
580
+ **Theme: agent が送ってきた `model` フィールドそのものを auto_router の判定軸に追加。** Claude Code / Cursor 等の agent 側設定 (Opus / Sonnet / Haiku の使い分け) を、CodeRouter 側 profile chain の選択にも反映できるようにする。`auto_router.rules[].if.model_pattern` を 5 番目の matcher として導入、既存 4 種 (`has_image` / `code_fence_ratio_min` / `content_contains` / `content_regex`) と同じ "exactly one" 規約と eager regex compile (typo は startup で fast-fail) を継承。
581
+
582
+ ユースケース例:
583
+
584
+ ```yaml
585
+ auto_router:
586
+ rules:
587
+ - if: { model_pattern: "claude-3-5-haiku.*" }
588
+ route_to: lightweight
589
+ - if: { model_pattern: "claude-3-5-sonnet.*" }
590
+ route_to: coding
591
+ default_rule_profile: writing
592
+ ```
593
+
594
+ agent 側で「モデルの使い分けは決まってる」状況に CodeRouter が綺麗に乗れる。`free-claude-code` repo の同様機能を CodeRouter の declarative auto_router framework に取り込んだ形。
595
+
596
+ - Tests: 833 → **838** (+5: Sonnet→coding / Haiku→lightweight / no-model field → fallthrough / 不正 regex は schema load で fast-fail / model_pattern と content rule の first-match-wins precedence)
597
+ - Runtime deps: 5 → 5 (30 sub-release 連続据え置き)
598
+ - Backward compat: 完全互換、既存 `auto_router` rule は何も変わらない、`model_pattern` を使わない deployment は挙動完全一致
599
+
600
+ #### Changes
601
+
602
+ - `coderouter/config/schemas.py`:
603
+ - `RuleMatcher` に `model_pattern: str | None = None` を追加、`_MATCHER_FIELDS` tuple に追加 (zero/multiple-fields の "exactly one" バリデータが自動適用)。
604
+ - `_compile_regex_eagerly` バリデータを `model_pattern` も覆うよう拡張、不正な regex は schema load で `ValueError("Invalid regex for model_pattern ...")` を発火 (`content_regex` と同じ fast-fail パターン)。
605
+ - docstring の Variants セクションに 5 番目として `model_pattern` を追記、`re.fullmatch` semantics と `content_regex` の `re.search` との違い (model 識別子は "structured tokens" であり全体描写型) を明示。
606
+
607
+ - `coderouter/routing/auto_router.py`:
608
+ - `_extract_model(body)` ヘルパを新設 — 両 ingress (Anthropic `/v1/messages` / OpenAI `/v1/chat/completions`) で body の top-level `model` field を 1 ヶ所で抽出、空文字列 / 非 str は None 扱い。
609
+ - `_match_rule(rule, message, text, model)` シグネチャに `model: str | None` を追加、`model_pattern` matcher を 5 番目の分岐として実装。`re.fullmatch` で評価 (model id は構造的 token なので部分一致より全体記述型の方が直観に合う)。`model is None` の時は False を返して fallthrough させる (空 body などの test fixtures 対策)。
610
+ - `classify(...)` 内で `_extract_model(body)` を一度だけ呼び、`_match_rule` に流す。`user_msg is None` でも `model_pattern` rule は評価する (空 messages でも model 経路で route 可能)。
611
+ - `_emit_resolved` / `_emit_fallthrough` の `signals` payload に `model` を追記、auto-router-resolved log で何の model id で routing 判断したかが dashboard / Prometheus exporter から見える。
612
+
613
+ - `tests/test_auto_router.py` Group 6 (per-model auto-routing) を新設、5 ケース:
614
+ - `test_classify_model_pattern_sonnet_routes_to_coding` — 基本ケース、`claude-3-5-sonnet.*` → coding profile。content は writing 寄りでも model rule が勝つ。
615
+ - `test_classify_model_pattern_haiku_routes_to_lightweight` — 4-profile fixture (`_model_pattern_config` で lightweight 追加)、Haiku id → lightweight profile。
616
+ - `test_classify_model_pattern_no_model_field_falls_through` — body に `model` field がない時、`r".+"` でも match せず default_rule_profile に落ちる (fixtures / test harness 用 robustness)。
617
+ - `test_model_pattern_invalid_regex_fast_fails_at_load` — `r"([unclosed"` → `RuleMatcher` 構築時に `ValueError(model_pattern)` (`content_regex` と同じ eager compile path)。
618
+ - `test_model_pattern_first_match_wins_over_later_content_rule` — model_pattern rule を content_contains rule より前に置くと、両方 match する body でも先勝、global "first match wins" を pin。
619
+
620
+ #### Files touched
621
+
622
+ ```
623
+ M CHANGELOG.md
624
+ M coderouter/config/schemas.py
625
+ M coderouter/routing/auto_router.py
626
+ M tests/test_auto_router.py
627
+ ```
628
+
629
+ #### Why now
630
+
631
+ `docs/inside/future.md §6.6` の v1.10 着手順序で 2 番目に推奨されていた quick win。実装規模 ~120 LOC (見積 ~150-200 LOC を下回って収束)、tests +5、半日工数。既存 auto_router framework の 1 matcher 追加なので構造的負債なし、`free-claude-code` 由来要望を CodeRouter の declarative 思想を崩さずに取り込めた。次の v1.10 候補 (provider 月次予算 / longContext auto-switch / v1.9-E phase 2) の前段階として位置付け。
632
+
633
+ ---
634
+
635
+ ### v1.9-B2: streaming 経路の usage 集約 (v1.10 候補 #1)
636
+
637
+ **Theme: v1.9.0 で意図的に v1.10 候補へ繰り越した quick win を回収。** v1.9.0a6 で「streaming パスでも `cache-observed` log を emit する」ところまでは揃えたが、token 数は `outcome=unknown` + ゼロ固定の placeholder だった。本 patch は `message_start.message.usage` + 終端 `message_delta.usage` を accumulator で max-merge 集約し、非 streaming (`generate_anthropic`) と同じ outcome 分類 + cost 計算 + ログ payload 形状に揃える。`/dashboard` / Prometheus / MetricsCollector 側は branch 不要で streaming 経路の数字が取れるようになる。
638
+
639
+ - Tests: 830 → **833** (+3: cache_hit / cache_creation / no_cache の streaming 集約 — `tests/test_fallback_cache_observed.py`)
640
+ - Runtime deps: 5 → 5 (29 sub-release 連続据え置き)
641
+ - Backward compat: 完全互換、log payload は v1.9-A と同じ schema、`streaming=true` flag のみ意味的に "観測値" になる (ゼロ placeholder ではなくなる)
642
+
643
+ #### Changes
644
+
645
+ - `coderouter/routing/fallback.py`:
646
+ - `_StreamUsageAccumulator` を新設 — `message_start.message.usage` と `message_delta.usage` から `input_tokens` / `output_tokens` / `cache_read_input_tokens` / `cache_creation_input_tokens` を per-field max-merge で集約。`output_tokens` は終端 `message_delta` で最終値が決まるため max が安全、cache fields は API minor version によって `message_start` / `message_delta` どちらにも現れる可能性があるため両方を観測。`usage_present` は「upstream が空 dict も含めて usage を返したか」を保持し、何も流れてこなかった streaming は引き続き `outcome=unknown` に分類。
647
+ - `_emit_cache_observed_streaming(...)` を追加 — accumulator 値を `classify_cache_outcome` / `compute_cost_for_attempt` に通して `log_cache_observed` を呼ぶ。非 streaming `_emit_cache_observed` と同じ outcome 分類 + cost 計算ロジック。
648
+ - `stream_anthropic(...)` 内のループで `acc = _StreamUsageAccumulator()` を初期化、`first` および後続 `event_iter` の各 event に `acc.observe(...)` を呼ぶ。完了時の `log_cache_observed(..., outcome="unknown", *=0)` を `_emit_cache_observed_streaming(acc, ..., provider_config=adapter.config)` に置換。
649
+ - `_emit_cache_observed` の docstring を更新 — `streaming=True` arg は openai_compat 経路 (downgrade で 1 つの response に collapse される) 用に残す説明に改訂。
650
+
651
+ - `tests/test_fallback_cache_observed.py`:
652
+ - `_CacheAnthropicAdapter.stream_anthropic` を constructor 引数駆動に変更 (`message_start.message.usage` に input_tokens + cache 系、`message_delta.usage` に input_tokens + output_tokens を流す、ゼロ時は空 dict を出して "usage 一切なし" を再現可能)。
653
+ - 既存 `test_cache_observed_fires_on_streaming_with_unknown_outcome` の docstring を v1.9-B2 文脈に更新 (上流から usage が 1 件も流れない時の `unknown` 床をピン留め)。
654
+ - 新規 3 ケース:
655
+ - `test_streaming_aggregates_cache_hit_usage` — `cache_read_input_tokens=2048` を含む stream → `outcome=cache_hit` + 入出力カウンタ集約。
656
+ - `test_streaming_aggregates_cache_creation_usage` — `cache_creation_input_tokens=1500` の stream → `outcome=cache_creation`。
657
+ - `test_streaming_aggregates_no_cache_outcome` — non-zero usage + cache fields なし → `outcome=no_cache` (本番最頻 case、v1.9.0a6 の placeholder では拾えていなかった)。
658
+
659
+ #### Why now
660
+
661
+ v1.9.0 GA ノートで明示した「v1.10 候補」のうち最も短期に取れる quick win。実装サイズ ~150 LOC、半日工数で `outcome=unknown` placeholder を観測値に置き換えられるため、cost dashboard / cache-hit rate panel の streaming 経路カバレッジが完成する。`v1.9-E phase 2` (L2/L5) や per-model auto-routing といった上位 priority 作業の前段で済ませておくと、その後の adaptive routing / Vision pillar 完成度が上がる。
662
+
663
+ #### Out of scope
664
+
665
+ - ChatRequest.stream() 経路 (OpenAI-shaped streaming) は対象外 — `stream_anthropic` の sibling であり、Anthropic 経由の cache observation は未対応の領域。Anthropic prompt cache を利用する client は実質 `/v1/messages` 経由なので影響範囲は限定的。
666
+ - v1.9.0a6 で論じた "downgrade 後の synthesize_anthropic_stream_from_response" 経路 — 元になる AnthropicResponse から `message_start` event が usage 付きで再構築されるため、accumulator が自動でカバーする (追加実装不要)。
667
+
668
+ #### Files touched
669
+
670
+ ```
671
+ M CHANGELOG.md
672
+ M coderouter/routing/fallback.py
673
+ M tests/test_fallback_cache_observed.py
674
+ ```
675
+
676
+ ---
677
+
9
678
  ## [v1.9.0] — 2026-04-29 (Umbrella tag — Cache observability + Adaptive routing + Cost-aware + Long-run reliability)
10
679
 
11
680
  **Theme: 「観測 → 理解 → 行動 → 信頼性」を 1 minor で揃える、observability pillar の成熟。** v1.9.0 は 6 sub-release (v1.9-A〜E) を通じて、CodeRouter を「動いてはいるが何が起きているか分からない」状態から、「**何にいくら使った / どこで遅くなった / 何で詰まった**」が運用ログ 1 行で分かる状態に押し上げる。具体的には:
@@ -841,7 +1510,7 @@ M coderouter/doctor.py
841
1510
  M pyproject.toml
842
1511
  M plan.md
843
1512
  M docs/troubleshooting.md
844
- M docs/articles/note-v1-8-1-reality-check.md (or new file v1-8-2)
1513
+ M docs/articles/v1-saga/note-1-v1-8-1-reality-check.md (or new file v1-8-2)
845
1514
  M tests/test_doctor.py
846
1515
  ```
847
1516