coderouter-cli 1.8.5__tar.gz → 1.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/CHANGELOG.md +654 -0
  2. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/PKG-INFO +7 -5
  3. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/README.en.md +6 -4
  4. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/README.md +6 -4
  5. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/config/capability_registry.py +26 -0
  6. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/config/schemas.py +161 -0
  7. coderouter_cli-1.9.0/coderouter/cost.py +154 -0
  8. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/data/model-capabilities.yaml +55 -0
  9. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/doctor.py +242 -0
  10. coderouter_cli-1.9.0/coderouter/guards/__init__.py +18 -0
  11. coderouter_cli-1.9.0/coderouter/guards/tool_loop.py +339 -0
  12. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/ingress/anthropic_routes.py +86 -0
  13. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/logging.py +261 -0
  14. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/metrics/collector.py +186 -0
  15. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/metrics/prometheus.py +123 -0
  16. coderouter_cli-1.9.0/coderouter/routing/adaptive.py +495 -0
  17. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/routing/capability.py +36 -18
  18. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/routing/fallback.py +328 -5
  19. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/pyproject.toml +1 -1
  20. coderouter_cli-1.9.0/tests/test_capability_registry_cache_control.py +235 -0
  21. coderouter_cli-1.9.0/tests/test_doctor_cache_probe.py +402 -0
  22. coderouter_cli-1.9.0/tests/test_fallback_cache_observed.py +440 -0
  23. coderouter_cli-1.9.0/tests/test_guards_tool_loop.py +398 -0
  24. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_ingress_anthropic.py +140 -0
  25. coderouter_cli-1.9.0/tests/test_metrics_cache.py +298 -0
  26. coderouter_cli-1.9.0/tests/test_metrics_cost.py +403 -0
  27. coderouter_cli-1.9.0/tests/test_metrics_prometheus_cache.py +158 -0
  28. coderouter_cli-1.9.0/tests/test_routing_adaptive.py +386 -0
  29. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/.gitignore +0 -0
  30. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/LICENSE +0 -0
  31. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/__init__.py +0 -0
  32. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/__main__.py +0 -0
  33. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/adapters/__init__.py +0 -0
  34. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/adapters/anthropic_native.py +0 -0
  35. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/adapters/base.py +0 -0
  36. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/adapters/openai_compat.py +0 -0
  37. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/adapters/registry.py +0 -0
  38. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/cli.py +0 -0
  39. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/cli_stats.py +0 -0
  40. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/config/__init__.py +0 -0
  41. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/config/env_file.py +0 -0
  42. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/config/loader.py +0 -0
  43. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/data/__init__.py +0 -0
  44. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/doctor_apply.py +0 -0
  45. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/env_security.py +0 -0
  46. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/errors.py +0 -0
  47. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/ingress/__init__.py +0 -0
  48. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/ingress/app.py +0 -0
  49. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/ingress/dashboard_routes.py +0 -0
  50. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/ingress/metrics_routes.py +0 -0
  51. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/ingress/openai_routes.py +0 -0
  52. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/metrics/__init__.py +0 -0
  53. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/output_filters.py +0 -0
  54. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/routing/__init__.py +0 -0
  55. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/routing/auto_router.py +0 -0
  56. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/translation/__init__.py +0 -0
  57. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/translation/anthropic.py +0 -0
  58. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/translation/convert.py +0 -0
  59. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/coderouter/translation/tool_repair.py +0 -0
  60. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/assets/dashboard-demo.png +0 -0
  61. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  62. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/designs/v1.6-auto-router-verification.md +0 -0
  63. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/designs/v1.6-auto-router.md +0 -0
  64. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/free-tier-guide.en.md +0 -0
  65. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/free-tier-guide.md +0 -0
  66. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/hf-ollama-models.md +0 -0
  67. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/llamacpp-direct.en.md +0 -0
  68. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/llamacpp-direct.md +0 -0
  69. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/lmstudio-direct.en.md +0 -0
  70. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/lmstudio-direct.md +0 -0
  71. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/openrouter-roster/README.md +0 -0
  72. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/openrouter-roster/latest.json +0 -0
  73. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/quickstart.en.md +0 -0
  74. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/quickstart.md +0 -0
  75. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/retrospectives/v0.4.md +0 -0
  76. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/retrospectives/v0.5-verify.md +0 -0
  77. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/retrospectives/v0.5.md +0 -0
  78. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/retrospectives/v0.6.md +0 -0
  79. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/retrospectives/v0.7.md +0 -0
  80. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/retrospectives/v1.0-verify.md +0 -0
  81. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/retrospectives/v1.0.md +0 -0
  82. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/security.en.md +0 -0
  83. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/security.md +0 -0
  84. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/troubleshooting.en.md +0 -0
  85. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/troubleshooting.md +0 -0
  86. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/usage-guide.en.md +0 -0
  87. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/usage-guide.md +0 -0
  88. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/when-do-i-need-coderouter.en.md +0 -0
  89. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/docs/when-do-i-need-coderouter.md +0 -0
  90. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/examples/.env.example +0 -0
  91. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/examples/providers.auto-custom.yaml +0 -0
  92. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/examples/providers.auto.yaml +0 -0
  93. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/examples/providers.note-2026.yaml +0 -0
  94. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/examples/providers.nvidia-nim.yaml +0 -0
  95. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/examples/providers.yaml +0 -0
  96. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/scripts/demo_traffic.sh +0 -0
  97. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/scripts/openrouter_roster_diff.py +0 -0
  98. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/scripts/verify_v0_5.sh +0 -0
  99. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/scripts/verify_v1_0.sh +0 -0
  100. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/__init__.py +0 -0
  101. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/conftest.py +0 -0
  102. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_adapter_anthropic.py +0 -0
  103. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_auto_router.py +0 -0
  104. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_capability.py +0 -0
  105. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_capability_degraded_payload.py +0 -0
  106. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_capability_registry.py +0 -0
  107. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_claude_code_suitability.py +0 -0
  108. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_cli.py +0 -0
  109. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_cli_stats.py +0 -0
  110. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_config.py +0 -0
  111. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_dashboard_endpoint.py +0 -0
  112. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_doctor.py +0 -0
  113. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_doctor_apply.py +0 -0
  114. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_env_file.py +0 -0
  115. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_env_security.py +0 -0
  116. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_errors.py +0 -0
  117. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_examples_yaml.py +0 -0
  118. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_fallback.py +0 -0
  119. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_fallback_anthropic.py +0 -0
  120. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_fallback_cache_control.py +0 -0
  121. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_fallback_misconfig_warn.py +0 -0
  122. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_fallback_paid_gate.py +0 -0
  123. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_fallback_thinking.py +0 -0
  124. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_ingress_profile.py +0 -0
  125. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_metrics_collector.py +0 -0
  126. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_metrics_endpoint.py +0 -0
  127. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_metrics_jsonl.py +0 -0
  128. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_metrics_prometheus.py +0 -0
  129. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_openai_compat.py +0 -0
  130. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_openrouter_roster_diff.py +0 -0
  131. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_output_filters.py +0 -0
  132. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_output_filters_adapters.py +0 -0
  133. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_reasoning_strip.py +0 -0
  134. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_setup_sh.py +0 -0
  135. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_tool_repair.py +0 -0
  136. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_translation_anthropic.py +0 -0
  137. {coderouter_cli-1.8.5 → coderouter_cli-1.9.0}/tests/test_translation_reverse.py +0 -0
@@ -6,6 +6,660 @@ versioning follows [SemVer](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [v1.9.0] — 2026-04-29 (Umbrella tag — Cache observability + Adaptive routing + Cost-aware + Long-run reliability)
10
+
11
+ **Theme: 「観測 → 理解 → 行動 → 信頼性」を 1 minor で揃える、observability pillar の成熟。** v1.9.0 は 6 sub-release (v1.9-A〜E) を通じて、CodeRouter を「動いてはいるが何が起きているか分からない」状態から、「**何にいくら使った / どこで遅くなった / 何で詰まった**」が運用ログ 1 行で分かる状態に押し上げる。具体的には:
12
+
13
+ - **観測 (v1.9-A)** — Anthropic prompt cache の hit/miss を全リクエストで `cache-observed` ログに記録、`/dashboard` から hit_rate / saved tokens が見える
14
+ - **透過 (v1.9-B)** — openai_compat 経路でも cache_control / thinking 等の Anthropic 拡張を可能な限り保持、不可能な場合は `capability-degraded` で明示
15
+ - **動的最適化 (v1.9-C)** — profile に `adaptive: true` を付けると、normally-fast な provider が一時的に遅くなったとき自動で後ろに送り、user-felt latency を保護
16
+ - **コスト把握 (v1.9-D)** — providers.yaml の `cost:` で USD pricing を宣言、cache savings は別計算 (LiteLLM 等の競合品が落としている粒度) で dashboard に出る
17
+ - **信頼性ガード (v1.9-E phase 1, L3)** — 同じツールを同じ引数で連続呼び出しする「stuck loop」を検出、profile-level policy (`warn` / `inject` / `break`) で対処
18
+
19
+ 最後の v1.9.0 GA では v1.9.0a6 以降の実機検証で発見された **L3 `break` action の ingress 取りこぼし** (`ToolLoopBreakError` が catch されず 500 が返っていた) を 400 + 構造化 detail に修正、両 ingress 経路 (非 streaming HTTPException / streaming SSE error event) で揃えました。
20
+
21
+ - Tests: 828 → **830** (+2: break action 非 streaming 400 / streaming SSE error event)
22
+ - Runtime deps: 5 → 5 (29 sub-release 連続据え置き)
23
+ - Backward compat: 完全互換、profile / providers.yaml / API 全部変化なし
24
+ - v1.9.0a1〜a6 をまとめての GA、各 sub-release の詳細は本ファイル下部の alpha entry を参照
25
+
26
+ ### Changes since v1.9.0a6 — E-4 break action の ingress 修正
27
+
28
+ #### `coderouter/guards/tool_loop.py`
29
+
30
+ - `ToolLoopBreakError.__init__` に `threshold: int` / `window: int` をキーワード必須で追加。ingress 側で 400 detail を組むときに config を再 lookup せずに済むよう、検出パラメータを exception 自体に carry させる
31
+ - docstring に「Anthropic ingress が catch して 400 + 構造化 detail に変換する」を明記 (a3 で約束していたが実装が伴っていなかった)
32
+
33
+ #### `coderouter/routing/fallback.py`
34
+
35
+ - `_apply_tool_loop_guard` の `raise ToolLoopBreakError(...)` で `threshold=profile.tool_loop_threshold, window=profile.tool_loop_window` を渡すよう更新
36
+
37
+ #### `coderouter/ingress/anthropic_routes.py`
38
+
39
+ - `ToolLoopBreakError` を import
40
+ - 非 streaming `messages()` に `except ToolLoopBreakError → HTTPException(status_code=400, detail=_tool_loop_break_detail(exc))` を追加。`detail` は flat dict:
41
+
42
+ ```json
43
+ {
44
+ "error": "tool_loop_detected",
45
+ "message": "tool loop detected on profile='test-loop-break': tool 'Read' repeated 3 times consecutively.",
46
+ "profile": "test-loop-break",
47
+ "tool_name": "Read",
48
+ "repeat_count": 3,
49
+ "threshold": 3,
50
+ "window": 5
51
+ }
52
+ ```
53
+
54
+ クライアントは `detail.error == "tool_loop_detected"` で branch 可能、`message` は `str(exc)` と同一でログ grep フレンドリー
55
+ - streaming `_anthropic_sse_iterator` に `except ToolLoopBreakError` ブランチを追加、Anthropic 標準 envelope (`error.type == "invalid_request_error"`) + `error.tool_loop` ネストで構造化フィールドを露出。HTTP は 200 のまま (StreamingResponse はヘッダ確定後で 4xx に切り替えられない、midstream-error と同じ事情)
56
+ - helper 2 つ: `_tool_loop_break_extension(exc)` (両形式で共有する detection payload) / `_tool_loop_break_detail(exc)` (非 streaming flat dict 構築)
57
+ - `args_canonical` は両形式から意図的に除外 (tool input にはユーザデータが含まれうるため、400 detail / SSE error event に流出させない)
58
+
59
+ #### Tests
60
+
61
+ - **`tests/test_ingress_anthropic.py`** + 2:
62
+ - `_LoopBreakingEngine` クラス + `client_and_loop_breaking_engine` fixture を追加
63
+ - `test_break_action_non_streaming_returns_400_with_structured_detail` — 400 + `detail.error="tool_loop_detected"` + 5 detection field + `args_canonical` 不在を検証
64
+ - `test_break_action_streaming_emits_invalid_request_error_event` — 200 + 単発 SSE error event + Anthropic 標準 envelope + `error.tool_loop` ネスト + `args_canonical` 不在を検証
65
+
66
+ ### v1.9 series summary
67
+
68
+ | sub | release | feature |
69
+ |---|---|---|
70
+ | a1 | v1.9-A | Cache Observability — `cache-observed` log + dashboard panel |
71
+ | a2 | v1.9-B | Cross-backend cache passthrough + capability gate + doctor cache probe |
72
+ | a3 | v1.9-E phase 1 | L3 Tool-loop detection guard (warn / inject / break) |
73
+ | a4 | v1.9-C | Adaptive Routing — health-based dynamic chain priority |
74
+ | a5 | v1.9-D | Cost-aware Dashboard — Anthropic prompt-cache aware |
75
+ | a6 | v1.9-A streaming patch | `_emit_cache_observed` を `stream_anthropic` に追加 (実装漏れ修正) |
76
+ | **GA** | **v1.9-E phase 1 patch** | **`break` action の ingress 400 取りこぼし修正** (本 entry) |
77
+
78
+ ### Real-machine verification (2026-04-29, LM Studio + ollama)
79
+
80
+ ```
81
+ E-2 (warn): tool-loop-detected ... action: "warn" → 200 OK + provider 応答
82
+ E-3 (inject): tool-loop-detected ... action: "inject" → system に hint 追加 + 200 OK
83
+ + cache_read_input_tokens: 453 (prefix キャッシュ命中)
84
+ E-4 (break, non-stream): 400 + {"detail":{"error":"tool_loop_detected","profile":"test-loop-break",
85
+ "tool_name":"Read","repeat_count":3,...}}
86
+ E-4 (break, stream): 200 + event: error
87
+ data: {"type":"error","error":{"type":"invalid_request_error",
88
+ "tool_loop":{"profile":"test-loop-break","repeat_count":3,...}}}
89
+
90
+ C (adaptive, 静止): 全 provider 同速 → static order 維持、`adaptive-routing-applied` 出ない
91
+ C (adaptive, 発火): サイズ差 chain (lmstudio 27B-dense 474ms / ollama qwen-coder-1.5b 134ms / openrouter-free n/a)
92
+ → global_median 304ms × 1.5 = 456ms、lmstudio 474ms ≥ 456ms → demote +1
93
+ → effective_order: [ollama-qwen-coder-1_5b, openrouter-free, lmstudio-...]
94
+ → 試験 4 回目から ollama-qwen-coder-1_5b 行きに切り替わって着地、
95
+ debounce 30s で oscillation も観察されず
96
+ ```
97
+
98
+ E-2/E-3 は a3 で観察済み、E-4 (両形式) と C 発火パスは GA 直前に実機で初観察。verification.md には MoE モデルの罠 (Qwen3.6-35B-A3B は active 3.8B で速い) と rolling-window タイミング制約の注意を後追いで加筆予定 (本リリースには含まず)。
99
+
100
+ ### Migration
101
+
102
+ 不要。**v1.8.x / v1.9.0a* からの自然なアップグレード**:
103
+
104
+ - `coderouter` コマンド名 / Python import 名 / providers.yaml の format / env 変数 / ingress URL すべて完全に同じ
105
+ - `tool_loop_action` を未指定または `warn` / `inject` で運用していた profile は挙動完全変化なし
106
+ - `tool_loop_action: break` を既に使っていた profile のみ status code が 5xx → 4xx に変化 (a3〜a6 では実装バグで 500 Internal Server Error が返っていた、1.9.0 で docstring が約束する 400 + 構造化 detail に修正)。実運用で `break` を本番投入していたケースは想定されにくく、検証用途であれば修正後の方が期待挙動
107
+
108
+ ### Out of scope (v1.10 以降)
109
+
110
+ v1.9 series は意図的に閉じる:
111
+
112
+ - **v1.9-B2** — `message_delta` event の usage 集約で、streaming 経路でも実 token 数 / cache_read / cache_creation を取得 (現状は `outcome=unknown` 固定)
113
+ - **v1.9-E phase 2** — L2 Memory pressure (LM Studio / ollama backend OOM 検知) / L5 Backend health (continuous probe + chain reorder)
114
+ - **v1.10-?** — plan.md §13 系 (multi-tenant routing, etc.) — 別 minor
115
+
116
+ ### Files touched
117
+
118
+ ```
119
+ M CHANGELOG.md
120
+ M coderouter/guards/tool_loop.py
121
+ M coderouter/ingress/anthropic_routes.py
122
+ M coderouter/routing/fallback.py
123
+ M pyproject.toml
124
+ M tests/test_ingress_anthropic.py
125
+ ```
126
+
127
+ ---
128
+
129
+ ## [v1.9.0a6] — 2026-04-28 (v1.9-A streaming パスの cache-observed emit 漏れ patch)
130
+
131
+ **Theme: 実機検証で発見した v1.9-A の小さな実装ギャップを潰す。** v1.9-A の CHANGELOG / `CacheOutcome` docstring で「streaming レスポンスは `outcome=unknown` で記録される」と約束していたが、`stream_anthropic` 経路に `_emit_cache_observed` の呼び出しが実装漏れしていた (非 streaming `generate_anthropic` のみ実装済み)。実機で `curl -N stream:true` を投げても JSONL に `cache-observed` event が現れない事で発覚。doc で約束していた動作に実装を揃える。
132
+
133
+ - Tests: 826 → **828** (+2: streaming 成功時 emit / streaming 失敗時 emit せず)
134
+ - Runtime deps: 5 → 5 (28 sub-release 連続据え置き)
135
+ - Backward compat: 完全互換、profile / API 全部変更なし
136
+ - Pre-release: `1.9.0a6`
137
+
138
+ ### Changes
139
+
140
+ #### `coderouter/routing/fallback.py` `stream_anthropic` に cache-observed emit を追加
141
+
142
+ - `_apply_tool_loop_guard` 直後に `request_had_cache_control = anthropic_request_has_cache_control(request)` を変数化 (v0.5-B の inline call と新規 emit 用 caller の二重評価を回避)
143
+ - successful stream の最後 (`async for ev in event_iter` 完走後、`return` の直前) に `log_cache_observed(...)` を呼ぶ
144
+ - `outcome="unknown"` (v1.9-B が `message_delta` 集約するまで streaming は usage 取得しない約束)
145
+ - `streaming=True`
146
+ - tokens は all 0 (engine は streaming 経路の usage を集約していない、cost も 0)
147
+ - 非 streaming `generate_anthropic` の挙動には影響なし
148
+
149
+ #### Tests
150
+
151
+ - **`tests/test_fallback_cache_observed.py`** + 2:
152
+ - `test_cache_observed_fires_on_streaming_with_unknown_outcome` — 成功 streaming で `outcome=unknown` / `streaming=True` / `request_had_cache_control=True` が記録される
153
+ - `test_cache_observed_streaming_does_not_fire_on_provider_failure` — provider 失敗時は emit しない (非 streaming と同じ contract)
154
+ - 上記のため `_CacheAnthropicAdapter.stream_anthropic` を `NotImplementedError` raise から「3 events (start / delta / stop) を yield する minimal stream」に拡張
155
+
156
+ ### Why
157
+
158
+ v1.9-A 検証中に「stream:true の curl を投げても `cache-observed` log が JSONL に出ない」を発見 (`docs/inside/verification.md` の A-3 検証パス)。v1.9-A の `CacheOutcome` docstring を読み直すと「streaming responses always pair with `outcome=unknown` until v1.9-B aggregates `message_delta`」と書いてあったが、実装が `generate_anthropic` のみで `stream_anthropic` には emit を入れ忘れていた。
159
+
160
+ これは **doc-implementation gap**: dashboard / metrics dashboard 利用者から見ると「streaming で動いているはずなのに observation が記録されない」という不整合になる。v1.9.0a6 は約束と実装を揃える小 patch。
161
+
162
+ 副次的効果として A-3 (`hit_rate=null when only `unknown` observations`) の実機検証もこの patch で初めて可能になった。
163
+
164
+ ### Migration
165
+
166
+ `pyproject.toml version 1.9.0a5 → 1.9.0a6`、`coderouter --version` は 1.9.0a6 を返す。**手元の `~/.coderouter/providers.yaml` は触らない限り完全に変化なし**。Streaming 経路のレスポンス内容も変化なし — log line が 1 件追加されるだけ。
167
+
168
+ ### Files touched
169
+
170
+ ```
171
+ M CHANGELOG.md
172
+ M coderouter/routing/fallback.py
173
+ M pyproject.toml
174
+ M tests/test_fallback_cache_observed.py
175
+ ```
176
+
177
+ ### Out of scope (v1.9-B 送り)
178
+
179
+ - `message_delta` event aggregation で streaming 時にも実 token 数 / cache_read / cache_creation を取得する → outcome を unknown 固定でなく実値で出せるようにする
180
+
181
+ ---
182
+
183
+ ## [v1.9.0a5] — 2026-04-28 (v1.9-D: Cost-aware Dashboard — Anthropic prompt-cache aware)
184
+
185
+ **Theme: 「いくら使ってる」を可視化、cache savings を別枠で。** v1.9-A で観測、v1.9-B で透過保証、v1.9-D で **金額に翻訳**。Anthropic の prompt-cache 価格モデル (cache_read 90% 割引、cache_creation 25% 増し) を最初から正確に実装、LiteLLM 競合品が **cache savings を別計算しない** 弱点を構造的にカバー。
186
+
187
+ `docs/inside/future.md` §5.5 の v1.9-D 範囲を実装。
188
+
189
+ - Tests: 811 → **826** (+15: pure compute_cost 8 / collector dispatch 4 / Prometheus exposition 3)
190
+ - Runtime deps: 5 → 5 (27 sub-release 連続据え置き)
191
+ - Backward compat: 完全互換、`providers.yaml` の `cost:` フィールドは optional (unset = 0 contribution)
192
+ - Pre-release: `1.9.0a5`
193
+
194
+ ### Changes
195
+
196
+ #### `coderouter/cost.py` 新規 (~150 LOC)
197
+
198
+ - `CostBreakdown` dataclass — per-attempt cost components (input/output/cache_read/cache_creation USD + total + savings)
199
+ - `compute_cost_for_attempt(cost_config, *, input_tokens, ..., cache_creation)` 純関数:
200
+ - 4 token bucket をそれぞれの rate で計算
201
+ - cache_read tokens を `input_rate × cache_read_discount` で割引
202
+ - cache_creation tokens を `input_rate × cache_creation_premium` で premium
203
+ - savings = `cache_read tokens × input_rate × (1 - cache_read_discount)` (cache_creation は premium なので savings には入らない)
204
+ - 負の token / None config / partial config に対する defensive 処理
205
+
206
+ #### Schema: `CostConfig` 新設
207
+
208
+ - **`coderouter/config/schemas.py`**: `CostConfig` BaseModel に `input_tokens_per_million` / `output_tokens_per_million` / `cache_read_discount=0.10` / `cache_creation_premium=1.25` を declare
209
+ - `ProviderConfig.cost: CostConfig | None = None` 追加 — opt-in、unset の provider (local 等) は dashboard に 0 contribution
210
+
211
+ #### Engine integration
212
+
213
+ - **`coderouter/routing/fallback.py`**: `_emit_cache_observed` を拡張、`provider_config: ProviderConfig | None = None` パラメータを受けて `compute_cost_for_attempt()` で per-attempt USD cost + savings を計算、log payload に折り込む
214
+ - `generate_anthropic` の call site で `adapter.config` を渡す
215
+
216
+ #### Logging schema 拡張
217
+
218
+ - **`coderouter/logging.py`** `CacheObservedPayload` に `cost_usd: float` / `cost_savings_usd: float` フィールド追加 (default 0.0、pre-v1.9-D caller は zero contribution で互換)
219
+ - `log_cache_observed` helper の signature にも optional kwargs 追加
220
+
221
+ #### MetricsCollector: per-provider cost aggregation
222
+
223
+ - **`coderouter/metrics/collector.py`**: `cache-observed` event の dispatch で cost を集計
224
+ - `_cost_total_usd: dict[str, float]` (per-provider)
225
+ - `_cost_savings_usd: dict[str, float]` (per-provider)
226
+ - `_cost_total_usd_aggregate: float` / `_cost_savings_usd_aggregate: float` (process-wide)
227
+ - `snapshot()` 拡張:
228
+ - `counters.cost_total_usd` / `cost_savings_usd` (per-provider dict)
229
+ - `counters.cost_total_usd_aggregate` / `cost_savings_usd_aggregate` (process-wide)
230
+ - 各 provider row に `cost: {total_usd, savings_usd}` panel
231
+ - `reset()` で v1.9-D state も clear
232
+ - 防御的: malformed cost values (str/None) → 0.0 default、handler は raise しない
233
+
234
+ #### Prometheus exposition
235
+
236
+ - **`coderouter/metrics/prometheus.py`**: 新 helper `_counter_float()` (float-valued counter、`.10g` formatter で trailing zero trim) + 2 つの新 metric:
237
+ - `coderouter_cost_total_usd_total{provider}` — cumulative USD billed
238
+ - `coderouter_cost_savings_usd_total{provider}` — cumulative cache savings USD
239
+
240
+ #### Tests (+15)
241
+
242
+ - **`tests/test_metrics_cost.py`** 新規:
243
+ - `compute_cost_for_attempt`: None config / no cache / cache read discount / cache creation premium / combined / negative tokens defensive / partial config (7)
244
+ - Collector dispatch: per-provider aggregation / zero cost no entry / per-row cost panel / reset / malformed values (5)
245
+ - Prometheus: HELP+TYPE / per-provider labels / `_total` suffix (3)
246
+
247
+ ### Why
248
+
249
+ `docs/inside/future.md` §5.5 で確立した「LiteLLM ですら未対応の cache savings 計算を最初から正確に実装」の具体実装。Anthropic 価格モデルを 4 token bucket × 4 multiplier で正確に表現、operator が「ローカル LLM 併用でいくら浮いたか」「Anthropic prompt cache でいくら節約できたか」を 1 画面で見える状態を実現。
250
+
251
+ **競合状況**:
252
+ - LiteLLM の cost tracker は `cache_read_input_tokens` を full input rate で billing (= overstate)、savings 別計算なし
253
+ - claude-code-router は cost tracking 自体なし
254
+ - v1.9-D は **Claude Code 系 OSS で唯一、cache-aware cost dashboard を持つ**
255
+
256
+ ### Migration
257
+
258
+ `pyproject.toml version 1.9.0a4 → 1.9.0a5`、`coderouter --version` は 1.9.0a5 を返す。**手元の `~/.coderouter/providers.yaml` は触らない限り完全に変化なし**。
259
+
260
+ 明示的に有効化する operator は paid provider に `cost:` ブロックを追加:
261
+
262
+ ```yaml
263
+ providers:
264
+ - name: anthropic-direct
265
+ kind: anthropic
266
+ base_url: https://api.anthropic.com
267
+ model: claude-sonnet-4-8
268
+ api_key_env: ANTHROPIC_API_KEY
269
+ paid: true
270
+ cost: # v1.9-D 新フィールド
271
+ input_tokens_per_million: 3.00
272
+ output_tokens_per_million: 15.00
273
+ cache_read_discount: 0.10 # default、省略可
274
+ cache_creation_premium: 1.25 # default、省略可
275
+ ```
276
+
277
+ `coderouter serve` 起動後、`/metrics.json` の `counters.cost_total_usd` / `cost_savings_usd` で per-provider cost を取得可能。Prometheus scrape は `coderouter_cost_total_usd_total{provider="anthropic-direct"}` で取れる。
278
+
279
+ ### Files touched
280
+
281
+ ```
282
+ M CHANGELOG.md
283
+ M coderouter/config/schemas.py
284
+ M coderouter/logging.py
285
+ M coderouter/metrics/collector.py
286
+ M coderouter/metrics/prometheus.py
287
+ M coderouter/routing/fallback.py
288
+ M pyproject.toml
289
+ A coderouter/cost.py
290
+ A tests/test_metrics_cost.py
291
+ ```
292
+
293
+ ### Out of scope (次回以降)
294
+
295
+ - **`/dashboard` HTML cost panel**: snapshot schema は揃ったが UI 描画は v1.9-D2 で
296
+ - **`coderouter stats --cost` TUI**: 5 行サマリ CLI コマンドは v1.9-D2 で
297
+ - **期間別累積 (1 day / 1 week / 1 month)**: 現在 process-lifetime のみ。期間集計は SQLite persistence と組み合わせて v1.10 候補
298
+ - **OpenAI-shaped engine paths のコスト集計**: Anthropic 非 streaming 経路のみ。OpenAI ingress + streaming 対応は v1.9-C2 と同じ follow-up
299
+
300
+ ---
301
+
302
+ ## [v1.9.0a4] — 2026-04-28 (v1.9-C: Adaptive Routing — health-based dynamic chain priority)
303
+
304
+ **Theme: 「平常時の最適化」を chain に持ち込む。** 静的に declare した `providers` 順序を、live observed の median latency / error rate に基づいて自動再優先化。L5 (v1.9-E phase 3 予定) は二値 (HEALTHY/UNHEALTHY) で crash 対応するのに対し、C は連続値 gradient で **平常時の遅さ** を吸収する。両方とも同じ observation stream から動くが、適用ロジックが直交。
305
+
306
+ `docs/inside/future.md` §5.4 の v1.9-C 範囲を MVP 実装。**Anthropic 非 streaming パスのみ** 対応 (v1.9-C2 で OpenAI-shaped + streaming follow-up 予定)。
307
+
308
+ - Tests: 795 → **811** (+16: stats 4 / no-demote 3 / latency demote 2 / error-rate demote 2 / debounce 2 / engine integration 2 / constants pin 1)
309
+ - Runtime deps: 5 → 5 (26 sub-release 連続据え置き)
310
+ - Backward compat: 完全互換、既存 profile は default の `adaptive: false` で従来挙動を維持
311
+ - Pre-release: `1.9.0a4`、`pip install --pre coderouter-cli` で取得可能
312
+
313
+ ### Changes
314
+
315
+ #### `coderouter/routing/adaptive.py` 新規 (~360 LOC)
316
+
317
+ - `AdaptiveAdjuster` クラス — per-process singleton (engine が 1 つ保持)
318
+ - `record_attempt(provider, *, latency_ms, success, now=None)` — observation 記録、append on each engine attempt
319
+ - `stats_for(provider, *, now=None) -> ProviderStats` — rolling-window から median latency + error rate 計算
320
+ - `compute_effective_order(adapters, *, now=None) -> list[BaseAdapter]` — 静的 chain → 動的順序、debounce 適用
321
+ - `_ProviderObservation` / `_AdjusterState` / `ProviderStats` データクラス
322
+ - `_apply_debounce` 内部メソッド — `last_committed_rank` 比較で debounce window 内の rank 変更を pinning (両方向、demote→promote と promote→demote 両方)
323
+ - 定数:
324
+ - `ROLLING_WINDOW_S = 60.0`
325
+ - `LATENCY_DEMOTE_FACTOR = 1.5` (median × 1.5 を超えたら -1 段)
326
+ - `ERROR_RATE_DEMOTE_THRESHOLD = 0.10` (10% 失敗で -2 段)
327
+ - `DEBOUNCE_S = 30.0`
328
+ - `MIN_SAMPLES_FOR_LATENCY = 3` / `MIN_SAMPLES_FOR_ERROR_RATE = 5`
329
+
330
+ #### Engine integration (`coderouter/routing/fallback.py`)
331
+
332
+ - `FallbackEngine.__init__` で `_adaptive_adjuster: AdaptiveAdjuster` を eager 構築。`@property` の `_adaptive` で lazy-fallback も用意 (legacy test `__new__` bypass パターンに対する resilience)
333
+ - `_resolve_anthropic_chain`: profile が `adaptive: true` のときに `_adaptive.compute_effective_order(base)` で chain を再優先化、その後 thinking-capable bucket logic に渡す
334
+ - `_profile_is_adaptive(profile_name)` ヘルパ — chain resolver と recording 側で同じ profile lookup を共有
335
+ - `generate_anthropic` の adapter 呼び出しを `time.monotonic()` で wrap、success/failure 両方で `record_attempt(...)` 呼び出し。auth-flavored failures (401/403) は latency_ms=None で記録 (短絡応答なので latency 信号として無意味)
336
+
337
+ #### Logging
338
+
339
+ - 新 event `adaptive-routing-applied` (info-level) — 静的 chain と effective chain order が異なるときのみ fire。payload に static_order / effective_order / per-provider stats を含む
340
+
341
+ #### Config schema
342
+
343
+ - `FallbackChain.adaptive: bool = False` 追加。既存 yaml はそのまま動く (default false)
344
+
345
+ #### Tests
346
+
347
+ - **`tests/test_routing_adaptive.py`** 新規 (+16 tests):
348
+ - **Stats**: unseen / median は success のみ / window roll-off / error rate zero on empty (4)
349
+ - **No demote**: empty chain / no obs / all fast (3)
350
+ - **Latency demote**: 1.5× threshold / min samples gate (2)
351
+ - **Error rate demote**: 10% threshold / min samples gate (2)
352
+ - **Debounce**: pin within window / release after window (2)
353
+ - **Engine integration**: static profile not invoking adjuster / adaptive profile invoking adjuster (2)
354
+ - **Constants pin**: ROLLING_WINDOW_S / LATENCY_DEMOTE_FACTOR / ERROR_RATE_DEMOTE_THRESHOLD / DEBOUNCE_S / MIN_SAMPLES_* (1)
355
+
356
+ ### Why
357
+
358
+ `docs/inside/future.md` §5.4 で確立した「task-based (auto_router、v1.6-A) + health-based (v1.9-C) の両軸対応」のうち health-based を実装。auto_router は request shape (intent) で profile を選ぶが、profile の chain 内 priority は static のまま。v1.9-C で chain 内 priority が live observed health に追従するようになり、両軸が初めて補完関係を成す。
359
+
360
+ **競合状況**: claude-code-router は task-based 単独、LiteLLM は session-cost-based、何れも latency-aware adaptive routing を持たない。CodeRouter は v1.9-C で **task-based + health-based 両軸** を持つ唯一の Claude Code 系 OSS という位置づけ。
361
+
362
+ ### Migration
363
+
364
+ `pyproject.toml version 1.9.0a3 → 1.9.0a4`、`coderouter --version` は 1.9.0a4 を返す。**手元の `~/.coderouter/providers.yaml` は触らない限り完全に変化なし**。新フィールド `adaptive: false` がデフォルトなので、既存 profile はゼロ変更で従来動作を維持。
365
+
366
+ 明示的に有効化する operator は profile に追加:
367
+
368
+ ```yaml
369
+ profiles:
370
+ - name: coding
371
+ providers:
372
+ - lmstudio-qwen3-5-9b
373
+ - ollama-gemma4-26b
374
+ - openrouter-free
375
+ adaptive: true # 平常時の latency / error rate に基づく動的優先度
376
+ ```
377
+
378
+ ### Files touched
379
+
380
+ ```
381
+ M CHANGELOG.md
382
+ M coderouter/config/schemas.py
383
+ M coderouter/routing/fallback.py
384
+ M pyproject.toml
385
+ A coderouter/routing/adaptive.py
386
+ A tests/test_routing_adaptive.py
387
+ ```
388
+
389
+ ### Out of scope (次回以降の v1.9-C2)
390
+
391
+ - **OpenAI-shaped engine paths**: `generate` / `stream` (非 Anthropic ingress) からの `record_attempt` 呼び出し。MVP では Anthropic 非 streaming のみカバー
392
+ - **Anthropic streaming**: `stream_anthropic` の latency 計測 (mid-stream success の境界をどこに置くか設計余地あり)
393
+ - **Dashboard panel**: `/dashboard` に effective chain order の可視化 (「static order vs current effective order」の差分強調表示)
394
+ - **MetricsCollector への adaptive 集計**: 現在は `adaptive-routing-applied` log のみ。将来 dashboard panel 用に reorder 回数 / 直近 reorder timestamp などを集計
395
+ - **L5 (v1.9-E phase 3)**: binary HEALTHY/UNHEALTHY backend swap。本実装の continuous gradient と棲み分け、両方とも同じ observation stream を消費する設計
396
+
397
+ ---
398
+
399
+ ## [v1.9.0a3] — 2026-04-28 (v1.9-E phase 1: L3 Tool-loop detection guard)
400
+
401
+ **Theme: Long-run reliability の最初の guard。** `docs/inside/future.md` §5.3 の v1.9-E は L2/L3/L5 の 3 系統障害を扱う 1-2 週間のまとまった作業。1 commit で全部やると重いので **L3 (Tool loop detection) → L2 (Memory pressure) → L5 (Backend health)** の 3 段階で alpha pre-release を切る。
402
+
403
+ L3 は最も isolated で HTTP 系の依存なし、~300 LOC、self-contained。「Claude Code を 8 時間連続で local LLM に向けて使っても止まらない」を訴求するための最初の具体実装。
404
+
405
+ - Tests: 779 → **795** (+16: pure detect 8 / inject mutation 3 / engine helper 5)
406
+ - Runtime deps: 5 → 5 (25 sub-release 連続据え置き)
407
+ - Backward compat: 完全互換、`providers.yaml` 編集不要 (新フィールドはすべて default 値あり)
408
+ - Pre-release: `1.9.0a3`、`pip install --pre coderouter-cli` で取得可能
409
+
410
+ ### Changes
411
+
412
+ #### `coderouter/guards/` 新パッケージ + L3 detector
413
+
414
+ - **`coderouter/guards/__init__.py`** 新規 — Long-run guards のパッケージドッジ。L2 / L5 が今後追加される予定地。
415
+ - **`coderouter/guards/tool_loop.py`** 新規 (~250 LOC):
416
+ - `detect_tool_loop(request, *, window, threshold) -> ToolLoopDetection | None` 純関数。直近 `window` 件の assistant `tool_use` ブロックの**末尾連続**で同一 `(name, args)` が `threshold` 回以上発生していると検知
417
+ - `ToolUseRecord` / `ToolLoopDetection` データクラス
418
+ - `inject_loop_break_hint(request, *, hint)` — system フィールドに hint を append (str / None / list-of-blocks の 3 形を吸収)
419
+ - `ToolLoopBreakError` (CodeRouterError 派生) — `break` action 用 exception
420
+ - `DEFAULT_LOOP_INJECT_HINT` 定数 — 「You appear to be calling the same tool with the same arguments repeatedly...」
421
+ - **canonical-form JSON 比較** (`json.dumps(args, sort_keys=True)`) で `{"a":1,"b":2}` と `{"b":2,"a":1}` を同一視
422
+ - **trailing-run only** 検出 — 過去に脱出済みの streak は無視 (現在状態のみが actionable)
423
+
424
+ #### Engine integration
425
+
426
+ - **`coderouter/routing/fallback.py`**: `_apply_tool_loop_guard(request, config)` ヘルパ追加。`generate_anthropic` / `stream_anthropic` の chain dispatch 直前で呼ばれる。Action 別の挙動:
427
+ - `warn`: log のみ、request はそのまま
428
+ - `inject`: log + `inject_loop_break_hint` で system 注入された新 request を返す
429
+ - `break`: log + `raise ToolLoopBreakError`
430
+ - profile lookup 失敗時は silent no-op (chain resolution が別経路で error を出すので二重診断にならない)
431
+
432
+ #### Config schema
433
+
434
+ - **`coderouter/config/schemas.py`** `FallbackChain` 拡張:
435
+ - `tool_loop_window: int = 5` (range 2-50)
436
+ - `tool_loop_threshold: int = 3` (range 2-50)
437
+ - `tool_loop_action: Literal["warn", "inject", "break"] = "warn"`
438
+ - 既存 profile はすべて default で warn-only として動作 → 既存 deployment はゼロ変更
439
+
440
+ #### Logging
441
+
442
+ - **`coderouter/logging.py`**: `tool-loop-detected` warn-level log shape を新設
443
+ - `ToolLoopDetectedPayload` TypedDict (profile / tool_name / repeat_count / threshold / window / action)
444
+ - `log_tool_loop_detected()` helper — 単一の chokepoint
445
+ - 3 つの action すべてが同じ log line を fire するので dashboard は detection 全件を捕捉できる (action は label として区別)
446
+
447
+ ### Why
448
+
449
+ `docs/inside/future.md` §1 で確立した Vision「Local LLM で agent を長時間回すための信頼性層」の P3 (Long-run Reliability) の最初の具体実装。L3 が最も isolated で実装シンプル / テスト容易 / 単独で価値があり、最初の sub-release に最適。
450
+
451
+ 「Claude Code が同じファイルを 5 回 Read し続ける」「Bash で同じコマンドを 3 回叩いて止まらない」というのは長時間 agent loop で頻出する典型症状で、L3 はその検知を request shape だけで完結させる (Claude Code は full conversation history を毎回送るので tail inspection で十分)。
452
+
453
+ **競合状況** (future.md §3 referenced): L3 を体系的に対処する Claude Code 系 OSS は 2026-04-27 時点で調査リスト中ゼロ。本実装は単独差別化軸として位置づく。
454
+
455
+ ### Migration
456
+
457
+ `pyproject.toml version 1.9.0a2 → 1.9.0a3`、`coderouter --version` は 1.9.0a3 を返す。**手元の `~/.coderouter/providers.yaml` は触らない限り完全に変化なし**。新 schema フィールドはすべて default 値ありなので、既存 yaml はそのままロード可能で、警告の挙動も warn level (ログ出力のみ) なので既存処理に副作用なし。
458
+
459
+ 明示的に有効化したい operator は profile に以下を追加:
460
+
461
+ ```yaml
462
+ profiles:
463
+ - name: long-running-agent
464
+ providers: [...]
465
+ tool_loop_window: 5
466
+ tool_loop_threshold: 3
467
+ tool_loop_action: inject # または warn / break
468
+ ```
469
+
470
+ ### Files touched
471
+
472
+ ```
473
+ M CHANGELOG.md
474
+ M coderouter/config/schemas.py
475
+ M coderouter/logging.py
476
+ M coderouter/routing/fallback.py
477
+ M pyproject.toml
478
+ A coderouter/guards/__init__.py
479
+ A coderouter/guards/tool_loop.py
480
+ A tests/test_guards_tool_loop.py
481
+ ```
482
+
483
+ ### Out of scope (次回以降の v1.9-E phase)
484
+
485
+ - **L2 (Memory pressure awareness)**: Ollama `/api/ps` / LM Studio `/v1/models` / llama.cpp `/proc/meminfo` 直読みで backend memory probe、95% 超で軽量 model に swap
486
+ - **L5 (Backend health continuous monitoring)**: 60s 周期の健康 probe、UNHEALTHY を chain 末尾に降格 / 復帰時に元 priority 戻し、dashboard に effective chain order
487
+ - **MetricsCollector への loop event 集計**: 現在は構造化 log のみ、将来 dashboard panel で「直近 24h の loop 検知 N 件」表示
488
+ - **inject hint の operator override**: 現在 `DEFAULT_LOOP_INJECT_HINT` のみ、将来 profile-level `tool_loop_inject_hint` で日本語化等可能に
489
+
490
+ ---
491
+
492
+ ## [v1.9.0a2] — 2026-04-28 (v1.9-B: Cross-backend cache passthrough + capability gate + doctor cache probe)
493
+
494
+ **Theme: v1.9-A の「観測」を「保証」へ。** capability registry に `cache_control` フィールドを新設し、Claude 4 family + LM Studio 経由 Qwen3.5/3.6 を bundled で宣言。doctor に新 probe `_probe_cache` を追加し、cache_control の round-trip (1 回目 creation → 2 回目 read) を実機 verify。
495
+
496
+ `docs/inside/future.md` §5.2 の v1.9-B 範囲を実装。挙動変更は capability gate 拡張のみで、既存の `provider_supports_cache_control` 呼び出しは下位互換 (registry 未宣言 anthropic-kind は引き続き True)。
497
+
498
+ - Tests: 759 → **779** (+20: registry resolution 12 / doctor cache probe 8)
499
+ - Runtime deps: 5 → 5 (24 sub-release 連続据え置き)
500
+ - Backward compat: 完全互換、`providers.yaml` / API 全部変更なし
501
+ - Pre-release: `1.9.0a2`、`pip install --pre coderouter-cli` で取得可能
502
+
503
+ ### Changes
504
+
505
+ #### Capability registry: `cache_control` フィールド新設
506
+
507
+ - **`coderouter/config/capability_registry.py`**: `RegistryCapabilities` / `ResolvedCapabilities` に `cache_control: bool | None` フィールド追加。lookup walker に同フィールドを追加 (first-match-per-flag 既存 semantics に従う)。
508
+ - **`coderouter/data/model-capabilities.yaml`**: bundled で 5 rule 宣言:
509
+ - `claude-opus-4-*` / `claude-sonnet-4-*` / `claude-haiku-4-*` (kind=anthropic): `cache_control: true` — api.anthropic.com で実機検証済 (2026-04-20、1321 tokens 書き / 1321 tokens 読み)
510
+ - `qwen3.5-*` / `qwen3.6-*` (kind=anthropic): `cache_control: true` — LM Studio 0.4.12 `/v1/messages` で v1.8.4 実機検証済 (`cache_read_input_tokens: 280` 観測)
511
+ - openai_compat 系は意図的に未宣言 (= None) → 既存の v0.5-B `capability-degraded reason=translation-lossy` log がそのまま fire
512
+
513
+ #### Capability gate: registry を consult
514
+
515
+ - **`coderouter/routing/capability.py`**: `provider_supports_cache_control` に `registry: CapabilityRegistry | None = None` kwarg を追加。解決順序を 3 段に:
516
+ 1. `provider.capabilities.prompt_cache: true` → True (explicit per-provider)
517
+ 2. registry の `cache_control: true|false` → 即決
518
+ 3. fallback: `provider.kind == "anthropic"` → True (pre-v1.9-B 互換)
519
+ - registry が `False` を返したら kind=anthropic でも False を返すので、upstream regression 時に operator が一時的に `cache_control: false` を user yaml で declare → `capability-degraded` log が fire するという escape hatch が成立
520
+
521
+ #### Doctor: `_probe_cache` 新 probe 追加
522
+
523
+ - **`coderouter/doctor.py`**: `_probe_cache` 関数を新設、orchestrator の最後 (streaming probe の後) に組み込み。auth fail 時の SKIP list にも追加。
524
+ - 動作: 同一 body (~1900 token system prompt + `cache_control: ephemeral`) を 2 回 POST、1 回目で `cache_creation_input_tokens > 0`、2 回目で `cache_read_input_tokens > 0` を期待
525
+ - **Verdict 4 種**:
526
+ - **OK**: 2 回目で read > 0 → cache_control 配管が end-to-end 機能している
527
+ - **NEEDS_TUNING**: 1 回目 creation 観測 / 2 回目 read=0 → TTL 短すぎ or cache key mismatch
528
+ - **NEEDS_TUNING**: 両方とも creation/read 観測なし → upstream が cache_control を silent ignore (Anthropic compat 不完全) or 1024 token 最低未達
529
+ - **SKIP**: not anthropic / 未宣言 / upstream 5xx / auth fail
530
+ - **Gate は意図的に tight**: 2 paid HTTP call を消費するので、registry に `cache_control: true` 明示宣言 OR `providers.yaml capabilities.prompt_cache: true` のときのみ実行。kind=anthropic だけで自動実行はしない (unverified model に対して無駄な call を避ける)
531
+
532
+ #### Tests
533
+
534
+ - **`tests/test_capability_registry_cache_control.py`** 新規 (+12): registry resolution 4 / capability gate 5 / bundled YAML 検証 3
535
+ - bundled が `claude-opus-4-8` / `claude-sonnet-4-7` / `claude-haiku-4-1` で `cache_control=true` を返すこと
536
+ - bundled が `qwen3.5-9b` / `qwen3.6-35b-a3b` で `cache_control=true` を返すこと
537
+ - bundled が `openai_compat` の `qwen2.5-coder:7b` で undeclared (None) のまま → translation-lossy gate fire を確実にする
538
+ - **`tests/test_doctor_cache_probe.py`** 新規 (+8): probe gate / OK round-trip / NEEDS_TUNING (no hit / no creation) / explicit prompt_cache opt-in / 1st call 5xx → SKIP / auth fail → SKIP
539
+
540
+ ### Why
541
+
542
+ v1.9-A で「観測」した cache の動作を、v1.9-B で **どの (kind, model) が cache_control を保証するか** という contract に格上げ。doctor cache probe は **どの競合 (LiteLLM / claude-code-router / etc.) にもない機能** で、operator が「LM Studio で本当に cache が効いてるのか」を 1 コマンドで確認できる単独差別化軸。
543
+
544
+ LM Studio 0.4.12 を bundled YAML に組み込んだのは、v1.8.4 で実機確認した「Anthropic compat `/v1/messages` 経由で `cache_read_input_tokens: 280` が end-to-end 透過する」という事実を CodeRouter として保証宣言する意味がある。Qwen3.5/3.6 を `kind: anthropic` で declare している operator なら、`coderouter doctor --check-model lmstudio-qwen3-5-9b-anthropic` で OK が出れば prompt caching 実利用可能、という保証関係。
545
+
546
+ ### Migration
547
+
548
+ `pyproject.toml version 1.9.0a1 → 1.9.0a2`、`coderouter --version` は 1.9.0a2 を返す。**手元の `~/.coderouter/providers.yaml` は触らない限り完全に変化なし**。
549
+
550
+ `provider_supports_cache_control` は kwarg `registry=None` を追加したので signature は backward-compatible (既存 caller は変更なし)。registry を consult した結果 `False` で hard-disable できるのが新機能だが、bundled YAML は positive 宣言のみ ship なので default 挙動は変化なし。
551
+
552
+ ### Files touched
553
+
554
+ ```
555
+ M CHANGELOG.md
556
+ M coderouter/config/capability_registry.py
557
+ M coderouter/data/model-capabilities.yaml
558
+ M coderouter/doctor.py
559
+ M coderouter/routing/capability.py
560
+ M pyproject.toml
561
+ A tests/test_capability_registry_cache_control.py
562
+ A tests/test_doctor_cache_probe.py
563
+ ```
564
+
565
+ ### Out of scope (次回以降)
566
+
567
+ - **v1.9-E (前倒し)**: Long-run Guards 三段 (L2 memory pressure / L3 tool loop / L5 backend health continuous) — Vision の核心実装
568
+ - **v1.9-C**: Adaptive Routing (rolling latency window + health-based dynamic priority)
569
+ - **v1.9-D**: Cost-aware Dashboard
570
+ - streaming aggregation: cache 観測の streaming 時 `outcome` 値を `cache_hit/creation/no_cache` に格上げ (v1.9-A の `unknown` から)
571
+
572
+ ---
573
+
574
+ ## [v1.9.0a1] — 2026-04-28 (v1.9-A: Cache Observability — Anthropic prompt caching を観測可能に)
575
+
576
+ **Theme: v1.9 シリーズ最初の alpha pre-release。Anthropic prompt caching の動作を CodeRouter 側で観測可能にし、`cache_read_input_tokens` / `cache_creation_input_tokens` を 4 分類 (cache_hit / cache_creation / no_cache / unknown) で per-provider 集計。**
577
+
578
+ `docs/inside/future.md` §5.1 の v1.9-A 範囲を実装。挙動は変えず、観測経路を追加するだけの安全な追加。LiteLLM の `cache_creation_input_tokens` undercounting バグ (future.md §3) を最初から避ける厳密 4 分類集計を導入。次の v1.9-B (cross-backend cache passthrough + capability gate / doctor cache probe) で能動的な cache 制御を追加予定。
579
+
580
+ - Tests: 737 → **759** (+22: classify_cache_outcome / collector dispatch / snapshot cache panel / Prometheus exposition / engine emission)
581
+ - Runtime deps: 5 → 5 (23 sub-release 連続据え置き)
582
+ - Backward compat: 完全互換、`providers.yaml` / `~/.coderouter/model-capabilities.yaml` / API 全部変更なし
583
+ - Pre-release: `1.9.0a1` の `a1` は PEP 440 alpha pre-release。`pip install --pre coderouter-cli` で取得可能。`v1.9.0` 正式版は v1.9-B/E/C/D も完了次第
584
+
585
+ ### Changes
586
+
587
+ #### `cache-observed` 構造化ログイベント新設
588
+
589
+ - **`coderouter/logging.py`**: `CacheOutcome` Literal + `CacheObservedPayload` TypedDict + `log_cache_observed()` helper + `classify_cache_outcome()` 4 分類関数を追加。
590
+ - `cache_hit`: `cache_read_input_tokens > 0` (cache 再利用、〜10% input rate)
591
+ - `cache_creation`: `cache_creation_input_tokens > 0` かつ hit ではない (cache 書き込み、〜125% input rate)
592
+ - `no_cache`: usage 受信したが cache フィールド 0/欠損 (cache_control 無し or upstream が握り潰した)
593
+ - `unknown`: response に usage block 自体無し (streaming / openai_compat 経由 / pre-v1.9-A upstream)
594
+ - **理由**: `provider-ok` event に cache フィールドを混ぜると downstream consumers (collector / JSONL mirror / tests) すべてが新 schema 検証必要。専用 event なら streaming 時の `outcome=unknown` も自然に表現できる
595
+
596
+ #### Engine (`fallback.py`): 成功 response 毎に cache-observed を emit
597
+
598
+ - **`coderouter/routing/fallback.py`**: `generate_anthropic` の `provider-ok` 直後に `_emit_cache_observed()` 呼び出しを追加。`AnthropicResponse.usage.model_extra` から `cache_read_input_tokens` / `cache_creation_input_tokens` を抽出 (Pydantic `extra="allow"` 経由でラウンドトリップ済み)。
599
+ - native Anthropic + LM Studio `/v1/messages` (`kind: anthropic`) → cache フィールド付き → 4 分類正しく出る
600
+ - openai_compat → anthropic 変換経由 → cache フィールド無し → `outcome=no_cache` or `unknown`
601
+ - streaming aggregation は v1.9-B 送り (`message_delta` イベント集約が必要)、v1.9-A では非 streaming パスのみ対応
602
+
603
+ #### MetricsCollector: per-provider cache 集計
604
+
605
+ - **`coderouter/metrics/collector.py`**: `cache-observed` event を dispatch table に追加。新カウンタ:
606
+ - `_cache_read_tokens: Counter[str]` (per-provider)
607
+ - `_cache_creation_tokens: Counter[str]` (per-provider)
608
+ - `_cache_outcomes: dict[str, Counter[str]]` (per-provider × 4-class)
609
+ - `_cache_read_tokens_total: int` / `_cache_creation_tokens_total: int` (aggregate、毎 event で incremental 更新、snapshot 時の re-fold コスト回避)
610
+ - `snapshot()` 拡張: `counters.cache_*` (per-provider + aggregate) + 各 provider row に `cache: {read_tokens, creation_tokens, outcomes, hit_rate, observations}` panel を追加
611
+ - **`hit_rate`** は `cache_hit / (cache_hit + cache_creation + no_cache)`、`unknown` は分母から除外 (signal 無しを 0% 表示するのを回避)
612
+ - 観測無しなら `hit_rate=None`、dashboard で「—」表示できる
613
+ - `reset()` で v1.9-A state も clear
614
+
615
+ #### Prometheus exposition: 3 つの新 counter
616
+
617
+ - **`coderouter/metrics/prometheus.py`**:
618
+ - `coderouter_cache_read_tokens_total{provider="..."}` — cache 再利用された input token 累計
619
+ - `coderouter_cache_creation_tokens_total{provider="..."}` — cache 書き込み input token 累計
620
+ - `coderouter_cache_observed_total{provider="...", outcome="cache_hit|cache_creation|no_cache|unknown"}` — 4 分類イベント数
621
+ - `hit_rate` を gauge で expose しないのは Prometheus 慣習に従い (`rate()` で derivative を計算する方が時間窓を正しく扱える)
622
+
623
+ #### Tests (+22)
624
+
625
+ - **`tests/test_metrics_cache.py`** (+11): `classify_cache_outcome` 4 cases / collector dispatch / snapshot cache panel / hit_rate=None for idle / unknown-only keeps None / reset clears state / 防御的非 int 受け入れ
626
+ - **`tests/test_metrics_prometheus_cache.py`** (+5): empty-snapshot HELP/TYPE / per-provider read/creation labels / outcome label pair / `_total` suffix
627
+ - **`tests/test_fallback_cache_observed.py`** (+6): cache_hit / cache_creation / no_cache outcome 別 / openai_compat 経路で no_cache or unknown / 失敗時 emit せず / chain fallthrough 時 winning provider のみ emit
628
+
629
+ ### Why
630
+
631
+ `docs/inside/future.md` §1 で確立した Vision「Local LLM で agent を長時間回すための信頼性層」の 3 pillar 中、**P1 Connection Stability** の核心要素である Anthropic prompt caching を **観測可能に** することが v1.9 シリーズの最初のステップ。LM Studio 0.4.12 の Anthropic 互換 `/v1/messages` 経由で v1.8.4 に observed した `cache_read_input_tokens: 280` を、CodeRouter 側で **per-provider hit 率として集計・可視化** できるようになった。
632
+
633
+ LiteLLM cluster は `cache_creation_input_tokens` を `no_cache` に丸めて undercount する既知バグ (future.md §3 referenced) があり、CodeRouter は最初から 4 分類厳密集計でこれを回避。Claude Code 特化 OSS の中で **唯一の cache 観測機能** として位置づけ。
634
+
635
+ ### Migration
636
+
637
+ `pyproject.toml version 1.8.5 → 1.9.0a1`、`coderouter --version` は 1.9.0a1 を返す。**手元の `~/.coderouter/providers.yaml` は触らない限り完全に変化なし**。
638
+
639
+ `/metrics.json` の counters / providers schema は **追加のみ** (新 key `cache_read_tokens` / `cache_creation_tokens` / `cache_outcomes`、provider rows に `cache` panel)、既存 dashboards は壊れない。Prometheus scraper は新メトリクス自動 discovery。
640
+
641
+ ### Files touched
642
+
643
+ ```
644
+ M CHANGELOG.md
645
+ M coderouter/logging.py
646
+ M coderouter/metrics/collector.py
647
+ M coderouter/metrics/prometheus.py
648
+ M coderouter/routing/fallback.py
649
+ M pyproject.toml
650
+ A tests/test_fallback_cache_observed.py
651
+ A tests/test_metrics_cache.py
652
+ A tests/test_metrics_prometheus_cache.py
653
+ ```
654
+
655
+ ### Out of scope (次回以降)
656
+
657
+ - **v1.9-B**: cross-backend cache passthrough + capability gate (`capabilities.cache_control` registry / doctor cache probe / openai_compat strip warn) — 「観測」から「保証」へ
658
+ - **v1.9-E (前倒し)**: Long-run Guards 三段 (L2 memory pressure / L3 tool loop / L5 backend health) — Vision の核心実装
659
+ - streaming aggregation: `message_delta` event を集約して streaming 時も `outcome=cache_hit/creation/no_cache` を出せるようにする (v1.9-B 範囲)
660
+
661
+ ---
662
+
9
663
  ## [v1.8.5] — 2026-04-28 (doctor NEEDS_TUNING メッセージを v1.8.3 thinking-aware budget の事実に揃える + `docs/lmstudio-direct.md` 新規)
10
664
 
11
665
  **Theme: 文言の整合 patch + ドキュメント補完。**v1.8.3 で `tool_calls` / `num_ctx` / `streaming` の 3 probe に thinking-aware budget (256 / 1024) を入れた。今回はその事実を NEEDS_TUNING 時の detail メッセージに反映し、operator が「probe budget が小さすぎたのでは」と疑う余地をなくす。あわせて v1.8.4 で実機検証した LM Studio 0.4.12 経由経路を `docs/llamacpp-direct.md` と対をなす形で `docs/lmstudio-direct.md` (+ `.en.md`) として正式化。