coderouter-cli 1.8.5__tar.gz → 1.10.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.
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/CHANGELOG.md +1189 -1
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/PKG-INFO +27 -8
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/README.en.md +26 -7
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/README.md +26 -7
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/config/capability_registry.py +26 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/config/schemas.py +315 -1
- coderouter_cli-1.10.0/coderouter/cost.py +154 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/data/model-capabilities.yaml +55 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/doctor.py +243 -1
- coderouter_cli-1.10.0/coderouter/guards/__init__.py +18 -0
- coderouter_cli-1.10.0/coderouter/guards/backend_health.py +208 -0
- coderouter_cli-1.10.0/coderouter/guards/memory_pressure.py +210 -0
- coderouter_cli-1.10.0/coderouter/guards/tool_loop.py +339 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/ingress/anthropic_routes.py +86 -0
- coderouter_cli-1.10.0/coderouter/logging.py +1014 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/metrics/collector.py +272 -0
- coderouter_cli-1.10.0/coderouter/metrics/prometheus.py +428 -0
- coderouter_cli-1.10.0/coderouter/routing/adaptive.py +495 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/routing/auto_router.py +118 -13
- coderouter_cli-1.10.0/coderouter/routing/budget.py +191 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/routing/capability.py +36 -18
- coderouter_cli-1.10.0/coderouter/routing/fallback.py +1489 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/llamacpp-direct.en.md +1 -1
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/llamacpp-direct.md +1 -1
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/lmstudio-direct.en.md +1 -1
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/lmstudio-direct.md +1 -1
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/pyproject.toml +1 -1
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_auto_router.py +303 -0
- coderouter_cli-1.10.0/tests/test_backend_health.py +413 -0
- coderouter_cli-1.10.0/tests/test_budget.py +351 -0
- coderouter_cli-1.10.0/tests/test_capability_registry_cache_control.py +235 -0
- coderouter_cli-1.10.0/tests/test_doctor_cache_probe.py +402 -0
- coderouter_cli-1.10.0/tests/test_fallback_cache_observed.py +569 -0
- coderouter_cli-1.10.0/tests/test_guards_tool_loop.py +398 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_ingress_anthropic.py +140 -0
- coderouter_cli-1.10.0/tests/test_memory_pressure.py +362 -0
- coderouter_cli-1.10.0/tests/test_metrics_cache.py +298 -0
- coderouter_cli-1.10.0/tests/test_metrics_cost.py +403 -0
- coderouter_cli-1.10.0/tests/test_metrics_prometheus_cache.py +158 -0
- coderouter_cli-1.10.0/tests/test_routing_adaptive.py +386 -0
- coderouter_cli-1.8.5/coderouter/logging.py +0 -401
- coderouter_cli-1.8.5/coderouter/metrics/prometheus.py +0 -221
- coderouter_cli-1.8.5/coderouter/routing/fallback.py +0 -611
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/.gitignore +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/LICENSE +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/__init__.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/__main__.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/adapters/anthropic_native.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/adapters/base.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/adapters/openai_compat.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/cli.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/config/loader.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/env_security.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/errors.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/ingress/openai_routes.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/output_filters.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/translation/convert.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/free-tier-guide.en.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/free-tier-guide.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/hf-ollama-models.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/quickstart.en.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/quickstart.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/security.en.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/security.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/troubleshooting.en.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/troubleshooting.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/usage-guide.en.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/usage-guide.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/docs/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/examples/.env.example +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/examples/providers.auto-custom.yaml +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/examples/providers.auto.yaml +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/examples/providers.note-2026.yaml +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/examples/providers.yaml +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/__init__.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/conftest.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_capability.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_cli.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_config.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_doctor.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_env_file.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_env_security.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_errors.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_fallback.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_output_filters.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-1.8.5 → coderouter_cli-1.10.0}/tests/test_translation_reverse.py +0 -0
|
@@ -6,6 +6,1194 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [v1.10.0] — 2026-05-01 (Umbrella tag — Cost enforcement + Long-run reliability completion + Auto-router feature complete)
|
|
10
|
+
|
|
11
|
+
**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) で経路が閉じた。
|
|
12
|
+
|
|
13
|
+
含まれる出荷 5 件 (`docs/inside/future.md §6.6` の v1.10 着手順序、本 release で全完了):
|
|
14
|
+
|
|
15
|
+
| # | sub-release | テーマ | LOC | tests | 出荷先 |
|
|
16
|
+
|---|---|---|---|---|---|
|
|
17
|
+
| 1 | **v1.9-B2** | streaming 経路の usage 集約 (`_StreamUsageAccumulator`、placeholder→観測値) | ~150 | +3 | v1.9.1 |
|
|
18
|
+
| 2 | **per-model auto-routing** | `RuleMatcher.model_pattern` (Opus/Sonnet/Haiku 分岐、free-claude-code 由来) | ~120 | +5 | v1.9.1 |
|
|
19
|
+
| 3 | **provider 月次予算上限** | `BudgetTracker` + `cost.monthly_budget_usd` (LiteLLM 由来 / v1.9-D 累積版) | ~250 | +8 | **v1.10.0** |
|
|
20
|
+
| 4 | **v1.9-E phase 2 (L2/L5)** | Memory pressure detector + Backend health 状態機械 (Vision pillar 完成) | ~370 | +27 | **v1.10.0** |
|
|
21
|
+
| 5 | **longContext auto-switch** | `RuleMatcher.content_token_count_min` (claude-code-router 由来) | ~80 | +5 | **v1.10.0** |
|
|
22
|
+
|
|
23
|
+
- Tests: 838 (v1.9.1) → **871** (+33: 本 minor 単独 +27 from v1.9-E phase 2 + 8 budget + 5 longContext から、v1.10.0 で純増 +33)
|
|
24
|
+
- Runtime deps: 5 → 5 (**33 sub-release 連続据え置き**) — 最初から守ってきた `fastapi / uvicorn / httpx / pydantic / pyyaml` のみ
|
|
25
|
+
- pyproject version: 1.9.1 → 1.10.0
|
|
26
|
+
|
|
27
|
+
### Pillar 別の達成
|
|
28
|
+
|
|
29
|
+
#### P2 Long-run Reliability (v1.9-E 系) — Vision の核心が揃う
|
|
30
|
+
|
|
31
|
+
6 系統障害体系 (`docs/inside/future.md §1`) のうち v1.x で取りに行くと宣言した分が完成:
|
|
32
|
+
|
|
33
|
+
| # | 障害 | v1.x 担当 | 状態 |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| **L1** | Context overflow | (v2.0-F) | ⏳ |
|
|
36
|
+
| **L2** | Memory pressure | v1.9-E phase 2 | ✅ v1.10.0 |
|
|
37
|
+
| **L3** | Tool loop | v1.9-E phase 1 | ✅ v1.9.0 |
|
|
38
|
+
| **L4** | Quality drift | (v2.0-G) | ⏳ |
|
|
39
|
+
| **L5** | Backend crash / health | v1.9-E phase 2 | ✅ v1.10.0 |
|
|
40
|
+
| **L6** | Mid-stream interrupt | v0.3-A 既存 + (v2.0-H 強化) | ✅ baseline |
|
|
41
|
+
|
|
42
|
+
L2/L3/L5 の 3 兄弟が `coderouter/guards/` 配下に並び、それぞれ `MemoryPressureGuard` / `_apply_tool_loop_guard` / `BackendHealthMonitor` が pure module として独立。engine 統合は `_observe_provider_failure` / `_observe_provider_success` の 2 chokepoint で済む綺麗な設計に着地。
|
|
43
|
+
|
|
44
|
+
#### Cost pillar (v1.9-D 系) — 観測 → 制約の経路が閉じる
|
|
45
|
+
|
|
46
|
+
| 段階 | sub-release | 役割 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| **観測** | v1.9-A | `cache-observed` log + cache hit/miss 4-class outcome |
|
|
49
|
+
| **観測のカバレッジ** | v1.9-B2 (v1.9.1) | streaming 経路まで完全カバー、placeholder ゼロ化 |
|
|
50
|
+
| **理解** | v1.9-D | per-provider USD cost + cache savings 別計算 (LiteLLM 既存品が落とす精度) |
|
|
51
|
+
| **制約** | **v1.10.0** | `monthly_budget_usd` で per-provider 月次 cap、UTC 暦月 in-memory bucketing |
|
|
52
|
+
|
|
53
|
+
v1.9.0 GA 時点で「観測の 4-class 精度」「LiteLLM 既存品より精度高い cost 計算」が CodeRouter の差別化軸として確立、v1.10.0 でそれを enforcement に活用できる経路が閉じた。
|
|
54
|
+
|
|
55
|
+
#### Auto-router (v1.6 系) — 6 matcher で feature complete
|
|
56
|
+
|
|
57
|
+
| # | matcher | 由来 | 出荷 |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| 1 | `has_image` | v1.6-A bundled | v1.6.0 |
|
|
60
|
+
| 2 | `code_fence_ratio_min` | v1.6-A bundled | v1.6.0 |
|
|
61
|
+
| 3 | `content_contains` | v1.6-A user-defined | v1.6.0 |
|
|
62
|
+
| 4 | `content_regex` | v1.6-A user-defined | v1.6.0 |
|
|
63
|
+
| 5 | `model_pattern` | free-claude-code 由来 | v1.9.1 |
|
|
64
|
+
| 6 | `content_token_count_min` | claude-code-router 由来 | **v1.10.0** |
|
|
65
|
+
|
|
66
|
+
declarative routing が「latest message の content / 画像 (per-turn signal) + request 全体の model id / token count (request-shape signal)」で完備、competitive analysis で抽出した v1.10 候補の取り込みは打ち止め、これ以降の追加は要望ドリブンで再開する想定。
|
|
67
|
+
|
|
68
|
+
### Migration
|
|
69
|
+
|
|
70
|
+
不要。**v1.9.1 / v1.9.0 / v1.9.0a* からの自然なアップグレード**:
|
|
71
|
+
|
|
72
|
+
- `coderouter` コマンド名 / Python import 名 / providers.yaml の format / env 変数 / ingress URL すべて完全に同じ
|
|
73
|
+
- 新しい 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 と挙動完全一致
|
|
74
|
+
- 新しい 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 追加するだけで対応可能
|
|
75
|
+
|
|
76
|
+
### Out of scope (v2.0 以降)
|
|
77
|
+
|
|
78
|
+
- **L1 Context overflow** → v2.0-F (semantic compression、context budget per-mode)
|
|
79
|
+
- **L4 Quality drift detection** → v2.0-G (response 品質 rolling window 観測)
|
|
80
|
+
- **L6 Mid-stream stitching 強化** → v2.0-H (resumable continuation)
|
|
81
|
+
- **Continuous probing** → v2.0-I (毎時/毎日 model ヘルスチェック、HF dataset 公開)
|
|
82
|
+
- **Persistent budget state** (sqlite / Redis) — 5-deps 不変原則で v1.x 範囲では却下
|
|
83
|
+
- **L5 active probing** (60s 間隔の能動 GET /api/version) — v2.0-I の領域、passive で 80% カバーできているため後回し
|
|
84
|
+
- **tiktoken / SentencePiece による正確なトークンカウント** — 5-deps 不変原則で却下
|
|
85
|
+
|
|
86
|
+
詳細は `docs/inside/future.md §7` を参照。
|
|
87
|
+
|
|
88
|
+
### Files touched
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
A coderouter/guards/backend_health.py
|
|
92
|
+
A coderouter/guards/memory_pressure.py
|
|
93
|
+
A coderouter/routing/budget.py
|
|
94
|
+
A tests/test_backend_health.py
|
|
95
|
+
A tests/test_budget.py
|
|
96
|
+
A tests/test_memory_pressure.py
|
|
97
|
+
M CHANGELOG.md
|
|
98
|
+
M coderouter/config/schemas.py
|
|
99
|
+
M coderouter/logging.py
|
|
100
|
+
M coderouter/metrics/collector.py
|
|
101
|
+
M coderouter/metrics/prometheus.py
|
|
102
|
+
M coderouter/routing/auto_router.py
|
|
103
|
+
M coderouter/routing/fallback.py
|
|
104
|
+
M docs/inside/future.md
|
|
105
|
+
M plan.md
|
|
106
|
+
M pyproject.toml
|
|
107
|
+
M tests/test_auto_router.py
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### v1.10 候補 #5: longContext auto-switch (claude-code-router 由来)
|
|
113
|
+
|
|
114
|
+
**Theme: コンテキスト窓圧迫の自動逃がし。** 長文プロンプト (会話ヒストリーの累積、コードベース貼り付け等) が来た時、context window の小さいモデル (200K Anthropic) ではなく 1M ctx の Gemini Flash 系へ自動切替する仕組み。`auto_router.rules[].if.content_token_count_min` を 6 番目の matcher として追加、既存 5 種と同じ "exactly one" 規約を継承。
|
|
115
|
+
|
|
116
|
+
ユースケース例:
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
auto_router:
|
|
120
|
+
rules:
|
|
121
|
+
- if: { content_token_count_min: 32000 }
|
|
122
|
+
route_to: longcontext
|
|
123
|
+
default_rule_profile: writing
|
|
124
|
+
|
|
125
|
+
profiles:
|
|
126
|
+
- name: longcontext
|
|
127
|
+
providers:
|
|
128
|
+
- openrouter-gemini-flash-free # 1M ctx
|
|
129
|
+
- anthropic-haiku-direct # 200K ctx
|
|
130
|
+
- name: writing
|
|
131
|
+
providers: [anthropic-sonnet-direct]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
agent が短いやり取りを 100 ターン続けて context が膨らんだ時、自動で 1M ctx チェーンに切り替わる。`free-claude-code` / `claude-code-router` 由来のニーズを CodeRouter の declarative auto_router framework に取り込んだ形。
|
|
135
|
+
|
|
136
|
+
#### 設計判断: char/4 ヒューリスティック vs tiktoken
|
|
137
|
+
|
|
138
|
+
token カウントは `len(text) // 4` の素朴ヒューリスティック (OpenAI 公式の rule of thumb)。**5-deps 不変原則** (`plan.md §5.4`) を守るため tiktoken / SentencePiece は導入しない。トレードオフ:
|
|
139
|
+
|
|
140
|
+
- **English 散文 / コード**: char/4 はやや緩い (実際は ~3.5/token)、`min` 比較なので大きい threshold で安全側に倒せる
|
|
141
|
+
- **CJK (日本語/中国語/韓国語)**: char/4 は **保守的にカウント不足** (実際は ~1.5-2 char/token)、つまり 100k 文字の日本語プロンプトを ~25k tokens と過小評価。これは積極的に context overflow を引き起こす方向ではないので fail-safe な誤差
|
|
142
|
+
- **トレードオフ判断**: tiktoken なら正確だが 100MB 級の依存追加、SentencePiece でも 50MB 級。CodeRouter は「個人開発者用の signal-based router」なので、operator が threshold を実機運用フィードバックで調整する前提のヒューリスティックで十分
|
|
143
|
+
|
|
144
|
+
#### Other matchers との違い
|
|
145
|
+
|
|
146
|
+
`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 では誤検出する。
|
|
147
|
+
|
|
148
|
+
- Tests: 866 → **871** (+5: long-prompt match / 短文 fallthrough / 全 messages walk / 負値 reject / first-match-wins precedence)
|
|
149
|
+
- Runtime deps: 5 → 5 (33 sub-release 連続据え置き)
|
|
150
|
+
- Backward compat: 完全互換、既存 `auto_router` rule は何も変わらない
|
|
151
|
+
|
|
152
|
+
#### Changes
|
|
153
|
+
|
|
154
|
+
- `coderouter/config/schemas.py`:
|
|
155
|
+
- `RuleMatcher` に `content_token_count_min: int | None = None` (`ge=1`) を追加、`_MATCHER_FIELDS` に登録 (既存の "exactly one" バリデータが自動適用、`ge=1` で 0/負値は schema load で reject)。
|
|
156
|
+
- docstring の Variants セクションに 6 番目として明記、char/4 ヒューリスティック + 全 messages 対象 (latest-only の他 matcher と差別化) + 5-deps トレードオフを文書化。
|
|
157
|
+
|
|
158
|
+
- `coderouter/routing/auto_router.py`:
|
|
159
|
+
- `_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 寄与。
|
|
160
|
+
- `_match_rule` に `estimated_tokens: int` パラメータを追加、6 番目の分岐として `content_token_count_min` 比較を実装。
|
|
161
|
+
- `classify(...)` 内で `_estimate_total_tokens(body)` を 1 回計算、ルール評価ループに流す。`_emit_resolved` / `_emit_fallthrough` の signals payload に `estimated_tokens` を追記、dashboard / Prometheus exporter から「何トークン推定でその profile に流れたか」が見える。
|
|
162
|
+
|
|
163
|
+
- `tests/test_auto_router.py` Group 7 を新設、5 ケース:
|
|
164
|
+
- `test_classify_long_prompt_routes_to_longcontext` — 200,000 chars (~50,000 tokens) → 32,000 threshold を超えて longcontext profile。
|
|
165
|
+
- `test_classify_short_prompt_below_threshold_falls_through` — 1,000 chars (~250 tokens) → default_rule_profile (writing) に落ちる。
|
|
166
|
+
- `test_classify_long_context_walks_all_messages_not_just_latest` — 長い会話 history + 短い最新ユーザー msg、latest-only matcher なら拾えないケースを longContext は拾うことを pin。
|
|
167
|
+
- `test_content_token_count_min_rejects_non_positive_at_load` — `0` / `-5` を `RuleMatcher` 構築時に reject (pydantic `ge=1`)。
|
|
168
|
+
- `test_long_context_first_match_wins_over_later_image_rule` — token-count rule を先に置けば長文+画像 body でも longcontext が勝つ、先勝順序を pin。
|
|
169
|
+
|
|
170
|
+
#### Files touched
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
M CHANGELOG.md
|
|
174
|
+
M coderouter/config/schemas.py
|
|
175
|
+
M coderouter/routing/auto_router.py
|
|
176
|
+
M tests/test_auto_router.py
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### Why now
|
|
180
|
+
|
|
181
|
+
`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` ヘルパだけ新設で済んだ)。
|
|
182
|
+
|
|
183
|
+
これで **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 完成)。
|
|
184
|
+
|
|
185
|
+
#### Out of scope (v2.0 以降 / 将来の精緻化)
|
|
186
|
+
|
|
187
|
+
- **tiktoken / SentencePiece による正確なトークンカウント** — 5-deps 不変原則で却下。実機運用で threshold tuning が困難になったら再検討。
|
|
188
|
+
- **Provider 別 context-window 自動推測** — `model-capabilities.yaml` に `max_context_tokens` を加えれば自動推測できる方向もあるが、operator の運用シナリオ次第なので明示宣言で十分。
|
|
189
|
+
- **動的 threshold (chain の最小 max_context_tokens に応じて)** — 同上、明示宣言で十分。
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### v1.10 候補 #4: v1.9-E phase 2 (L2 memory pressure + L5 backend health) — Vision 完成
|
|
194
|
+
|
|
195
|
+
**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 障害をカバーする。
|
|
196
|
+
|
|
197
|
+
#### L2: Memory pressure detection + cooldown
|
|
198
|
+
|
|
199
|
+
ローカル 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 する:
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
profiles:
|
|
203
|
+
- name: default
|
|
204
|
+
providers: [ollama-large, ollama-small, openrouter-fallback]
|
|
205
|
+
memory_pressure_action: skip # off / warn / skip (default: warn)
|
|
206
|
+
memory_pressure_cooldown_s: 120 # default 120s, 10〜3600 s
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`action=skip` の時、ollama-large が OOM → 120 秒間 ollama-large を chain から除外、ollama-small や openrouter-fallback に流れる → クールダウン明けで再度 ollama-large を試す。`action=warn` (default) は log のみ、`off` は完全に無効化 (zero overhead)。
|
|
210
|
+
|
|
211
|
+
#### L5: Backend health (consecutive failure state machine)
|
|
212
|
+
|
|
213
|
+
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):
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
profiles:
|
|
217
|
+
- name: default
|
|
218
|
+
providers: [ollama-local, anthropic-fallback]
|
|
219
|
+
backend_health_action: demote # off / warn / demote (default: warn)
|
|
220
|
+
backend_health_threshold: 3
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
v1.9-C の `adaptive` (rolling-window 連続観測 + debounce) とは直交関係 — adaptive が「徐々に遅くなった」勾配ケース、L5 が「突発 crash」二値ケース。両者重ね掛け可能で、両方 enable した chain では「latency 劣化 → adaptive demote」「crash → L5 demote」の両方の信号で並べ替え。
|
|
224
|
+
|
|
225
|
+
#### Numbers
|
|
226
|
+
|
|
227
|
+
- Tests: 839 → **866** (+27 累積、L2 +19 / L5 +8)
|
|
228
|
+
- Runtime deps: 5 → 5 (32 sub-release 連続据え置き)
|
|
229
|
+
- Backward compat: 完全互換、両 `*_action` のデフォルトは `warn` (= log のみ、行動変化なし)。`off` で完全無効化。既存 v1.9.x deployment は yaml 変更なしで自然継続
|
|
230
|
+
|
|
231
|
+
#### Changes
|
|
232
|
+
|
|
233
|
+
- `coderouter/guards/memory_pressure.py` 新設 (~170 LOC):
|
|
234
|
+
- `is_memory_pressure_error(exc)` — 純関数、9 種の OOM フレーズに対する case-insensitive substring match (Ollama / LM Studio / llama.cpp / 汎用 CUDA / Metal の実観測パターン)。
|
|
235
|
+
- `MemoryPressureGuard` — per-provider TTL cooldown tracker、`mark_pressured` / `is_pressured` / `pressured_until` API、`time.monotonic` ベースで wall-clock skew 耐性、tests は `now=` 引数で deterministic 注入。
|
|
236
|
+
|
|
237
|
+
- `coderouter/guards/backend_health.py` 新設 (~200 LOC):
|
|
238
|
+
- `BackendHealthMonitor` — per-provider 状態機械 (HEALTHY / DEGRADED / UNHEALTHY)、`record_attempt(success, threshold)` で観測、状態遷移時のみ `HealthTransition` を返す (no log spam on stable state)、threshold は per-call で profile 違いに対応。
|
|
239
|
+
- 状態機械の遷移ルール: 失敗 N 回 (= threshold) で DEGRADED、2N 回で UNHEALTHY、1 回成功で即 HEALTHY 復帰。
|
|
240
|
+
|
|
241
|
+
- `coderouter/config/schemas.py`:
|
|
242
|
+
- `FallbackChain` に `memory_pressure_action` / `memory_pressure_cooldown_s` (L2) と `backend_health_action` / `backend_health_threshold` (L5) を追加。L3 (`tool_loop_*`) と同じ命名 + 同じ "off / warn / 行動" tri-state パターン。
|
|
243
|
+
- 各 field に `Literal` 型 + range 制約 + 詳細 docstring (どの障害を見るか、L2/L5 の使い分け、v1.9-C adaptive との関係)。
|
|
244
|
+
|
|
245
|
+
- `coderouter/logging.py`:
|
|
246
|
+
- L2: `log_memory_pressure_detected` / `log_skip_memory_pressure` / `log_chain_memory_pressure_blocked` ヘルパ + 3 つの TypedDict payload。paid-gate / budget-gate と完全 symmetric。
|
|
247
|
+
- L5: `log_backend_health_changed` (state transition、payload に old_state/new_state/consecutive_failures) / `log_demote_unhealthy_provider` ヘルパ + 2 つの TypedDict。
|
|
248
|
+
|
|
249
|
+
- `coderouter/routing/fallback.py`:
|
|
250
|
+
- `FallbackEngine` に `_memory_pressure` / `_backend_health` lazy property を追加 (`_adaptive` / `_budget` と同じパターン、`__new__` 経由 legacy tests 対応)。
|
|
251
|
+
- `_observe_provider_failure(provider, exc, profile)` ヘルパ — L2 OOM 検出 + L5 失敗カウンタを single chokepoint で dispatch、6 つの failure site (4 entry point × non-stream/mid-stream) から呼ぶ。
|
|
252
|
+
- `_observe_provider_success(provider, profile)` 新設 — L5 状態機械の成功遷移を 4 success site (provider-ok 時) から呼ぶ。
|
|
253
|
+
- `_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 抑制)。
|
|
254
|
+
|
|
255
|
+
- `coderouter/metrics/collector.py`:
|
|
256
|
+
- `_provider_skipped_memory_pressure: Counter` + `_chain_memory_pressure_blocked_total: int` (L2)。
|
|
257
|
+
- `_provider_demoted_unhealthy: Counter` + `_backend_health_transitions: dict[str, Counter]` (L5、transition を destination state でキー)。
|
|
258
|
+
- `skip-memory-pressure` / `chain-memory-pressure-blocked` / `backend-health-changed` / `demote-unhealthy-provider` event の dispatch + snapshot/reset 配線。
|
|
259
|
+
|
|
260
|
+
- `coderouter/metrics/prometheus.py`:
|
|
261
|
+
- `coderouter_provider_skipped_total{reason="memory_pressure"}` を既存 `paid` / `unknown` / `budget` と同 counter に同居。
|
|
262
|
+
- `coderouter_provider_demoted_unhealthy_total{provider}` (L5)、`coderouter_backend_health_transitions_total{provider, state}` (L5)、`coderouter_chain_memory_pressure_blocked_total` (L2) を追加。
|
|
263
|
+
|
|
264
|
+
- `tests/test_memory_pressure.py` 新設 (~360 LOC、+19 tests):
|
|
265
|
+
- **Group 1 (detector)**: 8 種の OOM フレーズを parameterize で網羅、5 種の非 OOM 失敗で false 確認。
|
|
266
|
+
- **Group 2 (guard)**: TTL cooldown / lazy expiry / re-mark 拡張。
|
|
267
|
+
- **Group 3 (engine)**: action=warn は log only / action=skip は cooldown 中 chain skip + fallback / action=off は完全無効 / 全 provider pressured で `chain-memory-pressure-blocked` warn + `NoProvidersAvailableError`。
|
|
268
|
+
|
|
269
|
+
- `tests/test_backend_health.py` 新設 (~340 LOC、+8 tests):
|
|
270
|
+
- **Group 1 (monitor)**: 初期状態 HEALTHY、threshold/2x threshold での状態遷移、success で UNHEALTHY → HEALTHY 即復帰、stable state で transition 返さない。
|
|
271
|
+
- **Group 2 (engine action)**: warn は log only / demote で chain reorder (try-provider 順序検証) / off で監視ゼロ / UNHEALTHY → HEALTHY recovery transition log。
|
|
272
|
+
- **Group 3 (chain reorder)**: 全 provider UNHEALTHY 時は demote no-op (log spam なし、best-effort 続行)。
|
|
273
|
+
|
|
274
|
+
#### Files touched
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
A coderouter/guards/backend_health.py
|
|
278
|
+
A coderouter/guards/memory_pressure.py
|
|
279
|
+
A tests/test_backend_health.py
|
|
280
|
+
A tests/test_memory_pressure.py
|
|
281
|
+
M CHANGELOG.md
|
|
282
|
+
M coderouter/config/schemas.py
|
|
283
|
+
M coderouter/logging.py
|
|
284
|
+
M coderouter/metrics/collector.py
|
|
285
|
+
M coderouter/metrics/prometheus.py
|
|
286
|
+
M coderouter/routing/fallback.py
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
#### Why now
|
|
290
|
+
|
|
291
|
+
`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 の到達点として位置付け。
|
|
292
|
+
|
|
293
|
+
#### Out of scope (v2.0 以降)
|
|
294
|
+
|
|
295
|
+
- **L5 active probing** (60s 間隔の能動 GET /api/version) — 受動 observation で十分カバーできる範囲、active probe を加えると httpx の lifecycle / mocking の複雑度が増えるため v2.0-I (`continuous probing` pillar 拡張) で再検討。
|
|
296
|
+
- **L2 thresholding (count of OOM events before mark)** — single OOM = mark の素朴実装で十分。複数 OOM 観測でしか mark しないという調整は実機運用 feedback が来てから。
|
|
297
|
+
- **HEALTHY/DEGRADED/UNHEALTHY の 4 段階以上化** — 3 段階で十分、運用 feedback が来てから検討。
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
### v1.10 候補 #3: provider 月次予算上限 (LiteLLM 由来 / v1.9-D の累積版)
|
|
302
|
+
|
|
303
|
+
**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 するようにする。
|
|
304
|
+
|
|
305
|
+
ユースケース例:
|
|
306
|
+
|
|
307
|
+
```yaml
|
|
308
|
+
providers:
|
|
309
|
+
- name: anthropic-direct
|
|
310
|
+
kind: anthropic
|
|
311
|
+
base_url: https://api.anthropic.com
|
|
312
|
+
model: claude-sonnet-4-6
|
|
313
|
+
cost:
|
|
314
|
+
input_tokens_per_million: 3.0
|
|
315
|
+
output_tokens_per_million: 15.0
|
|
316
|
+
monthly_budget_usd: 5.0 # ← v1.10 新フィールド
|
|
317
|
+
- name: ollama-local
|
|
318
|
+
base_url: http://localhost:11434/v1
|
|
319
|
+
model: qwen3.6:35b-a3b
|
|
320
|
+
# 無料 / cost 未設定 = 無制限 (skip 対象外)
|
|
321
|
+
profiles:
|
|
322
|
+
- name: default
|
|
323
|
+
providers: [anthropic-direct, ollama-local] # paid → free fallback
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`anthropic-direct` が今月 5 USD 消費した時点で chain resolver が skip し、`ollama-local` (無料) に fall through する。`skip-budget-exceeded` info + (全 provider が cap に達した時のみ) `chain-budget-exceeded` warn が emit される。
|
|
327
|
+
|
|
328
|
+
**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) で受ければ十分カバー可能。
|
|
329
|
+
|
|
330
|
+
- Tests: 831 → **839** (+8: BudgetTracker pure 3 / CostConfig schema 2 / engine integration 3)
|
|
331
|
+
- Runtime deps: 5 → 5 (31 sub-release 連続据え置き)
|
|
332
|
+
- Backward compat: 完全互換、`monthly_budget_usd` 未設定 deployment は挙動完全一致 (opt-in feature)
|
|
333
|
+
|
|
334
|
+
#### Changes
|
|
335
|
+
|
|
336
|
+
- `coderouter/config/schemas.py`:
|
|
337
|
+
- `CostConfig` に `monthly_budget_usd: float | None = None` を追加 (`ge=0.0`、None = 無制限)。
|
|
338
|
+
- docstring で UTC calendar-month + in-memory only persistence を明示、5-deps 不変原則との整合 (no sqlite/Redis) を文書化。
|
|
339
|
+
|
|
340
|
+
- `coderouter/routing/budget.py` 新設 (~190 LOC):
|
|
341
|
+
- `BudgetTracker` クラス — per-provider current-month USD running total を `dict[str, float]` で保持、`threading.RLock` 配下。月境界判定は `_utc_month_key` ヘルパ (UTC `datetime.now()` 経由、tests は `now=` 引数で deterministic に注入可能)。
|
|
342
|
+
- 公開 API: `record(provider, cost_usd)` / `is_over_budget(provider, budget_usd)` / `current_month()` / `total_for_provider(provider)` / `reset()`。
|
|
343
|
+
- **Lazy month rollover**: 各 public call の入口で `_roll_if_needed` を呼び、cached month と current UTC month が違えば `_totals` を clear してから query に答える。background timer 不要。
|
|
344
|
+
- `is_over_budget` は `>=` 比較 — exact-hit の "5.00 USD" は exhausted と判定 (conservative: 次の call は bill しない)。
|
|
345
|
+
|
|
346
|
+
- `coderouter/logging.py`:
|
|
347
|
+
- `SkipBudgetExceededPayload` / `ChainBudgetExceededPayload` TypedDict + `log_skip_budget_exceeded` / `log_chain_budget_exceeded` ヘルパを追加。`log_chain_paid_gate_blocked` のパターンを完全 mirror、payload に `month` (YYYY-MM UTC bucket) を含める。
|
|
348
|
+
|
|
349
|
+
- `coderouter/routing/fallback.py`:
|
|
350
|
+
- `FallbackEngine.__init__` に `_budget_tracker: BudgetTracker = BudgetTracker()` を追加、`_adaptive` と同じ lazy property pattern で `_budget` を露出 (legacy tests の `__new__` 経路でも空 tracker が返る)。
|
|
351
|
+
- `_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。
|
|
352
|
+
- `_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` を渡す配線。
|
|
353
|
+
|
|
354
|
+
- `coderouter/metrics/collector.py`:
|
|
355
|
+
- `_provider_skipped_budget: Counter[str]` + `_chain_budget_exceeded_total: int` を追加、`_provider_skipped_paid` / `_chain_paid_gate_blocked_total` の対称配置。
|
|
356
|
+
- `_dispatch` に `skip-budget-exceeded` / `chain-budget-exceeded` event の handler を追加。`reset()` / `snapshot()` も両 counter を含むように拡張。
|
|
357
|
+
- module docstring の event inventory に v1.10 行 2 件追記。
|
|
358
|
+
|
|
359
|
+
- `coderouter/metrics/prometheus.py`:
|
|
360
|
+
- `coderouter_provider_skipped_total{provider, reason="budget"}` を既存の `paid` / `unknown` と同じ counter に同居 (dashboards が reason 別 stack できるように)。
|
|
361
|
+
- `coderouter_chain_budget_exceeded_total` scalar counter を新設 (`coderouter_chain_paid_gate_blocked_total` の対称配置)。
|
|
362
|
+
|
|
363
|
+
- `tests/test_budget.py` 新設 (~340 LOC、+8 tests):
|
|
364
|
+
- **Group 1 (BudgetTracker pure)**: record 蓄積 / is_over_budget の `>=` boundary semantics / 月境界 rollover (`now=` 引数で April→May 跨ぎを deterministic に検証)。
|
|
365
|
+
- **Group 2 (CostConfig schema)**: `monthly_budget_usd: 5.0` 受理、負値 reject (pydantic `ge=0.0`)。
|
|
366
|
+
- **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)。
|
|
367
|
+
|
|
368
|
+
#### Files touched
|
|
369
|
+
|
|
370
|
+
```
|
|
371
|
+
A coderouter/routing/budget.py
|
|
372
|
+
A tests/test_budget.py
|
|
373
|
+
M CHANGELOG.md
|
|
374
|
+
M coderouter/config/schemas.py
|
|
375
|
+
M coderouter/logging.py
|
|
376
|
+
M coderouter/metrics/collector.py
|
|
377
|
+
M coderouter/metrics/prometheus.py
|
|
378
|
+
M coderouter/routing/fallback.py
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
#### Why now
|
|
382
|
+
|
|
383
|
+
`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」として割り切ることで構造的負債を避ける。
|
|
384
|
+
|
|
385
|
+
#### Out of scope
|
|
386
|
+
|
|
387
|
+
- **Persistent budget state** (sqlite / Redis / disk-backed) — 5-deps 不変原則により未対応。durable enforcement 必要なケースは v1.9-D dashboard を外部 alerting に繋ぐ運用で代替。
|
|
388
|
+
- **Rolling 30-day window** — UTC calendar month で十分 (typical billing cycle と一致、月境界判定の rollover 実装が単純)。rolling window は `_utc_month_key` を date-windowed key に差し替えれば追加できるが、operator request が来てから判断。
|
|
389
|
+
- **Per-profile budget** (vs per-provider) — provider 単位で十分。同じ provider を複数 profile が共有する場合 budget は共有されるべき (実コストの帰属先は provider なので) という意味的にも provider 帰属が正しい。
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## [v1.9.1] — 2026-05-01 (Patch — v1.10 候補から quick win 2 件先行刈取り)
|
|
394
|
+
|
|
395
|
+
**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 / 依存追加なし。
|
|
396
|
+
|
|
397
|
+
含まれる出荷 2 件 (`docs/inside/future.md §6.6` の v1.10 着手順序 #1, #2):
|
|
398
|
+
|
|
399
|
+
| # | sub-release | テーマ | LOC | tests |
|
|
400
|
+
|---|---|---|---|---|
|
|
401
|
+
| 1 | **v1.9-B2** | streaming 経路の usage 集約 — `_StreamUsageAccumulator` + `_emit_cache_observed_streaming` で `outcome=unknown` placeholder を観測値に置換 | ~150 | +3 |
|
|
402
|
+
| 2 | **per-model auto-routing** | `RuleMatcher.model_pattern` 5 番目 matcher 追加、`re.fullmatch` で body model id を評価 (free-claude-code 由来) | ~120 | +5 |
|
|
403
|
+
|
|
404
|
+
- Tests: 830 → **838** (+8 累積、v1.9-B2 +3 / per-model +5)
|
|
405
|
+
- Runtime deps: 5 → 5 (30 sub-release 連続据え置き)
|
|
406
|
+
- Backward compat: 完全互換、既存 yaml / API / log payload 全部既存と同じ schema、新フィールド (`model_pattern`) を使わない deployment は挙動完全一致
|
|
407
|
+
- pyproject version: 1.9.0 → 1.9.1
|
|
408
|
+
|
|
409
|
+
### Migration
|
|
410
|
+
|
|
411
|
+
不要。**v1.9.0 / v1.9.0a* からの自然なアップグレード**:
|
|
412
|
+
|
|
413
|
+
- `coderouter` コマンド名 / Python import 名 / providers.yaml の format / env 変数 / ingress URL すべて完全に同じ
|
|
414
|
+
- 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 は同じ、ロジック変更不要
|
|
415
|
+
- `auto_router.rules[].if.model_pattern` を使い始めるには yaml に 1 行足すだけ、既存 rule に影響なし
|
|
416
|
+
|
|
417
|
+
### Out of scope (v1.10 / v1.9.x 続編)
|
|
418
|
+
|
|
419
|
+
[v1.9.0] GA ノートと `docs/inside/future.md §6.6` で示した v1.10 候補から残り 3 件:
|
|
420
|
+
|
|
421
|
+
- **provider 月次予算上限** (LiteLLM 由来、v1.9-D の累積版) — `monthly_budget_usd` で provider 単位の running total + 超過時 skip + log。~400 LOC、3-5 日。
|
|
422
|
+
- **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 週間。
|
|
423
|
+
- **longContext auto-switch** — `auto_router` rule type 5 として `content_token_count_min` matcher 追加 (claude-code-router task-based 取込)。~200 LOC、3-5 日。
|
|
424
|
+
|
|
425
|
+
これら 3 件は構造拡張を伴うため v1.9.1 patch ではなく v1.10.0 minor で個別 sub-release にして出荷する想定。
|
|
426
|
+
|
|
427
|
+
### Files touched
|
|
428
|
+
|
|
429
|
+
```
|
|
430
|
+
M CHANGELOG.md
|
|
431
|
+
M coderouter/config/schemas.py
|
|
432
|
+
M coderouter/routing/auto_router.py
|
|
433
|
+
M coderouter/routing/fallback.py
|
|
434
|
+
M docs/inside/future.md
|
|
435
|
+
M plan.md
|
|
436
|
+
M pyproject.toml
|
|
437
|
+
M tests/test_auto_router.py
|
|
438
|
+
M tests/test_fallback_cache_observed.py
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
### per-model auto-routing (v1.10 候補 #2、free-claude-code 由来)
|
|
444
|
+
|
|
445
|
+
**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) を継承。
|
|
446
|
+
|
|
447
|
+
ユースケース例:
|
|
448
|
+
|
|
449
|
+
```yaml
|
|
450
|
+
auto_router:
|
|
451
|
+
rules:
|
|
452
|
+
- if: { model_pattern: "claude-3-5-haiku.*" }
|
|
453
|
+
route_to: lightweight
|
|
454
|
+
- if: { model_pattern: "claude-3-5-sonnet.*" }
|
|
455
|
+
route_to: coding
|
|
456
|
+
default_rule_profile: writing
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
agent 側で「モデルの使い分けは決まってる」状況に CodeRouter が綺麗に乗れる。`free-claude-code` repo の同様機能を CodeRouter の declarative auto_router framework に取り込んだ形。
|
|
460
|
+
|
|
461
|
+
- 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)
|
|
462
|
+
- Runtime deps: 5 → 5 (30 sub-release 連続据え置き)
|
|
463
|
+
- Backward compat: 完全互換、既存 `auto_router` rule は何も変わらない、`model_pattern` を使わない deployment は挙動完全一致
|
|
464
|
+
|
|
465
|
+
#### Changes
|
|
466
|
+
|
|
467
|
+
- `coderouter/config/schemas.py`:
|
|
468
|
+
- `RuleMatcher` に `model_pattern: str | None = None` を追加、`_MATCHER_FIELDS` tuple に追加 (zero/multiple-fields の "exactly one" バリデータが自動適用)。
|
|
469
|
+
- `_compile_regex_eagerly` バリデータを `model_pattern` も覆うよう拡張、不正な regex は schema load で `ValueError("Invalid regex for model_pattern ...")` を発火 (`content_regex` と同じ fast-fail パターン)。
|
|
470
|
+
- docstring の Variants セクションに 5 番目として `model_pattern` を追記、`re.fullmatch` semantics と `content_regex` の `re.search` との違い (model 識別子は "structured tokens" であり全体描写型) を明示。
|
|
471
|
+
|
|
472
|
+
- `coderouter/routing/auto_router.py`:
|
|
473
|
+
- `_extract_model(body)` ヘルパを新設 — 両 ingress (Anthropic `/v1/messages` / OpenAI `/v1/chat/completions`) で body の top-level `model` field を 1 ヶ所で抽出、空文字列 / 非 str は None 扱い。
|
|
474
|
+
- `_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 対策)。
|
|
475
|
+
- `classify(...)` 内で `_extract_model(body)` を一度だけ呼び、`_match_rule` に流す。`user_msg is None` でも `model_pattern` rule は評価する (空 messages でも model 経路で route 可能)。
|
|
476
|
+
- `_emit_resolved` / `_emit_fallthrough` の `signals` payload に `model` を追記、auto-router-resolved log で何の model id で routing 判断したかが dashboard / Prometheus exporter から見える。
|
|
477
|
+
|
|
478
|
+
- `tests/test_auto_router.py` Group 6 (per-model auto-routing) を新設、5 ケース:
|
|
479
|
+
- `test_classify_model_pattern_sonnet_routes_to_coding` — 基本ケース、`claude-3-5-sonnet.*` → coding profile。content は writing 寄りでも model rule が勝つ。
|
|
480
|
+
- `test_classify_model_pattern_haiku_routes_to_lightweight` — 4-profile fixture (`_model_pattern_config` で lightweight 追加)、Haiku id → lightweight profile。
|
|
481
|
+
- `test_classify_model_pattern_no_model_field_falls_through` — body に `model` field がない時、`r".+"` でも match せず default_rule_profile に落ちる (fixtures / test harness 用 robustness)。
|
|
482
|
+
- `test_model_pattern_invalid_regex_fast_fails_at_load` — `r"([unclosed"` → `RuleMatcher` 構築時に `ValueError(model_pattern)` (`content_regex` と同じ eager compile path)。
|
|
483
|
+
- `test_model_pattern_first_match_wins_over_later_content_rule` — model_pattern rule を content_contains rule より前に置くと、両方 match する body でも先勝、global "first match wins" を pin。
|
|
484
|
+
|
|
485
|
+
#### Files touched
|
|
486
|
+
|
|
487
|
+
```
|
|
488
|
+
M CHANGELOG.md
|
|
489
|
+
M coderouter/config/schemas.py
|
|
490
|
+
M coderouter/routing/auto_router.py
|
|
491
|
+
M tests/test_auto_router.py
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
#### Why now
|
|
495
|
+
|
|
496
|
+
`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) の前段階として位置付け。
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
### v1.9-B2: streaming 経路の usage 集約 (v1.10 候補 #1)
|
|
501
|
+
|
|
502
|
+
**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 経路の数字が取れるようになる。
|
|
503
|
+
|
|
504
|
+
- Tests: 830 → **833** (+3: cache_hit / cache_creation / no_cache の streaming 集約 — `tests/test_fallback_cache_observed.py`)
|
|
505
|
+
- Runtime deps: 5 → 5 (29 sub-release 連続据え置き)
|
|
506
|
+
- Backward compat: 完全互換、log payload は v1.9-A と同じ schema、`streaming=true` flag のみ意味的に "観測値" になる (ゼロ placeholder ではなくなる)
|
|
507
|
+
|
|
508
|
+
#### Changes
|
|
509
|
+
|
|
510
|
+
- `coderouter/routing/fallback.py`:
|
|
511
|
+
- `_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` に分類。
|
|
512
|
+
- `_emit_cache_observed_streaming(...)` を追加 — accumulator 値を `classify_cache_outcome` / `compute_cost_for_attempt` に通して `log_cache_observed` を呼ぶ。非 streaming `_emit_cache_observed` と同じ outcome 分類 + cost 計算ロジック。
|
|
513
|
+
- `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)` に置換。
|
|
514
|
+
- `_emit_cache_observed` の docstring を更新 — `streaming=True` arg は openai_compat 経路 (downgrade で 1 つの response に collapse される) 用に残す説明に改訂。
|
|
515
|
+
|
|
516
|
+
- `tests/test_fallback_cache_observed.py`:
|
|
517
|
+
- `_CacheAnthropicAdapter.stream_anthropic` を constructor 引数駆動に変更 (`message_start.message.usage` に input_tokens + cache 系、`message_delta.usage` に input_tokens + output_tokens を流す、ゼロ時は空 dict を出して "usage 一切なし" を再現可能)。
|
|
518
|
+
- 既存 `test_cache_observed_fires_on_streaming_with_unknown_outcome` の docstring を v1.9-B2 文脈に更新 (上流から usage が 1 件も流れない時の `unknown` 床をピン留め)。
|
|
519
|
+
- 新規 3 ケース:
|
|
520
|
+
- `test_streaming_aggregates_cache_hit_usage` — `cache_read_input_tokens=2048` を含む stream → `outcome=cache_hit` + 入出力カウンタ集約。
|
|
521
|
+
- `test_streaming_aggregates_cache_creation_usage` — `cache_creation_input_tokens=1500` の stream → `outcome=cache_creation`。
|
|
522
|
+
- `test_streaming_aggregates_no_cache_outcome` — non-zero usage + cache fields なし → `outcome=no_cache` (本番最頻 case、v1.9.0a6 の placeholder では拾えていなかった)。
|
|
523
|
+
|
|
524
|
+
#### Why now
|
|
525
|
+
|
|
526
|
+
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 完成度が上がる。
|
|
527
|
+
|
|
528
|
+
#### Out of scope
|
|
529
|
+
|
|
530
|
+
- ChatRequest.stream() 経路 (OpenAI-shaped streaming) は対象外 — `stream_anthropic` の sibling であり、Anthropic 経由の cache observation は未対応の領域。Anthropic prompt cache を利用する client は実質 `/v1/messages` 経由なので影響範囲は限定的。
|
|
531
|
+
- v1.9.0a6 で論じた "downgrade 後の synthesize_anthropic_stream_from_response" 経路 — 元になる AnthropicResponse から `message_start` event が usage 付きで再構築されるため、accumulator が自動でカバーする (追加実装不要)。
|
|
532
|
+
|
|
533
|
+
#### Files touched
|
|
534
|
+
|
|
535
|
+
```
|
|
536
|
+
M CHANGELOG.md
|
|
537
|
+
M coderouter/routing/fallback.py
|
|
538
|
+
M tests/test_fallback_cache_observed.py
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## [v1.9.0] — 2026-04-29 (Umbrella tag — Cache observability + Adaptive routing + Cost-aware + Long-run reliability)
|
|
544
|
+
|
|
545
|
+
**Theme: 「観測 → 理解 → 行動 → 信頼性」を 1 minor で揃える、observability pillar の成熟。** v1.9.0 は 6 sub-release (v1.9-A〜E) を通じて、CodeRouter を「動いてはいるが何が起きているか分からない」状態から、「**何にいくら使った / どこで遅くなった / 何で詰まった**」が運用ログ 1 行で分かる状態に押し上げる。具体的には:
|
|
546
|
+
|
|
547
|
+
- **観測 (v1.9-A)** — Anthropic prompt cache の hit/miss を全リクエストで `cache-observed` ログに記録、`/dashboard` から hit_rate / saved tokens が見える
|
|
548
|
+
- **透過 (v1.9-B)** — openai_compat 経路でも cache_control / thinking 等の Anthropic 拡張を可能な限り保持、不可能な場合は `capability-degraded` で明示
|
|
549
|
+
- **動的最適化 (v1.9-C)** — profile に `adaptive: true` を付けると、normally-fast な provider が一時的に遅くなったとき自動で後ろに送り、user-felt latency を保護
|
|
550
|
+
- **コスト把握 (v1.9-D)** — providers.yaml の `cost:` で USD pricing を宣言、cache savings は別計算 (LiteLLM 等の競合品が落としている粒度) で dashboard に出る
|
|
551
|
+
- **信頼性ガード (v1.9-E phase 1, L3)** — 同じツールを同じ引数で連続呼び出しする「stuck loop」を検出、profile-level policy (`warn` / `inject` / `break`) で対処
|
|
552
|
+
|
|
553
|
+
最後の v1.9.0 GA では v1.9.0a6 以降の実機検証で発見された **L3 `break` action の ingress 取りこぼし** (`ToolLoopBreakError` が catch されず 500 が返っていた) を 400 + 構造化 detail に修正、両 ingress 経路 (非 streaming HTTPException / streaming SSE error event) で揃えました。
|
|
554
|
+
|
|
555
|
+
- Tests: 828 → **830** (+2: break action 非 streaming 400 / streaming SSE error event)
|
|
556
|
+
- Runtime deps: 5 → 5 (29 sub-release 連続据え置き)
|
|
557
|
+
- Backward compat: 完全互換、profile / providers.yaml / API 全部変化なし
|
|
558
|
+
- v1.9.0a1〜a6 をまとめての GA、各 sub-release の詳細は本ファイル下部の alpha entry を参照
|
|
559
|
+
|
|
560
|
+
### Changes since v1.9.0a6 — E-4 break action の ingress 修正
|
|
561
|
+
|
|
562
|
+
#### `coderouter/guards/tool_loop.py`
|
|
563
|
+
|
|
564
|
+
- `ToolLoopBreakError.__init__` に `threshold: int` / `window: int` をキーワード必須で追加。ingress 側で 400 detail を組むときに config を再 lookup せずに済むよう、検出パラメータを exception 自体に carry させる
|
|
565
|
+
- docstring に「Anthropic ingress が catch して 400 + 構造化 detail に変換する」を明記 (a3 で約束していたが実装が伴っていなかった)
|
|
566
|
+
|
|
567
|
+
#### `coderouter/routing/fallback.py`
|
|
568
|
+
|
|
569
|
+
- `_apply_tool_loop_guard` の `raise ToolLoopBreakError(...)` で `threshold=profile.tool_loop_threshold, window=profile.tool_loop_window` を渡すよう更新
|
|
570
|
+
|
|
571
|
+
#### `coderouter/ingress/anthropic_routes.py`
|
|
572
|
+
|
|
573
|
+
- `ToolLoopBreakError` を import
|
|
574
|
+
- 非 streaming `messages()` に `except ToolLoopBreakError → HTTPException(status_code=400, detail=_tool_loop_break_detail(exc))` を追加。`detail` は flat dict:
|
|
575
|
+
|
|
576
|
+
```json
|
|
577
|
+
{
|
|
578
|
+
"error": "tool_loop_detected",
|
|
579
|
+
"message": "tool loop detected on profile='test-loop-break': tool 'Read' repeated 3 times consecutively.",
|
|
580
|
+
"profile": "test-loop-break",
|
|
581
|
+
"tool_name": "Read",
|
|
582
|
+
"repeat_count": 3,
|
|
583
|
+
"threshold": 3,
|
|
584
|
+
"window": 5
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
クライアントは `detail.error == "tool_loop_detected"` で branch 可能、`message` は `str(exc)` と同一でログ grep フレンドリー
|
|
589
|
+
- streaming `_anthropic_sse_iterator` に `except ToolLoopBreakError` ブランチを追加、Anthropic 標準 envelope (`error.type == "invalid_request_error"`) + `error.tool_loop` ネストで構造化フィールドを露出。HTTP は 200 のまま (StreamingResponse はヘッダ確定後で 4xx に切り替えられない、midstream-error と同じ事情)
|
|
590
|
+
- helper 2 つ: `_tool_loop_break_extension(exc)` (両形式で共有する detection payload) / `_tool_loop_break_detail(exc)` (非 streaming flat dict 構築)
|
|
591
|
+
- `args_canonical` は両形式から意図的に除外 (tool input にはユーザデータが含まれうるため、400 detail / SSE error event に流出させない)
|
|
592
|
+
|
|
593
|
+
#### Tests
|
|
594
|
+
|
|
595
|
+
- **`tests/test_ingress_anthropic.py`** + 2:
|
|
596
|
+
- `_LoopBreakingEngine` クラス + `client_and_loop_breaking_engine` fixture を追加
|
|
597
|
+
- `test_break_action_non_streaming_returns_400_with_structured_detail` — 400 + `detail.error="tool_loop_detected"` + 5 detection field + `args_canonical` 不在を検証
|
|
598
|
+
- `test_break_action_streaming_emits_invalid_request_error_event` — 200 + 単発 SSE error event + Anthropic 標準 envelope + `error.tool_loop` ネスト + `args_canonical` 不在を検証
|
|
599
|
+
|
|
600
|
+
### v1.9 series summary
|
|
601
|
+
|
|
602
|
+
| sub | release | feature |
|
|
603
|
+
|---|---|---|
|
|
604
|
+
| a1 | v1.9-A | Cache Observability — `cache-observed` log + dashboard panel |
|
|
605
|
+
| a2 | v1.9-B | Cross-backend cache passthrough + capability gate + doctor cache probe |
|
|
606
|
+
| a3 | v1.9-E phase 1 | L3 Tool-loop detection guard (warn / inject / break) |
|
|
607
|
+
| a4 | v1.9-C | Adaptive Routing — health-based dynamic chain priority |
|
|
608
|
+
| a5 | v1.9-D | Cost-aware Dashboard — Anthropic prompt-cache aware |
|
|
609
|
+
| a6 | v1.9-A streaming patch | `_emit_cache_observed` を `stream_anthropic` に追加 (実装漏れ修正) |
|
|
610
|
+
| **GA** | **v1.9-E phase 1 patch** | **`break` action の ingress 400 取りこぼし修正** (本 entry) |
|
|
611
|
+
|
|
612
|
+
### Real-machine verification (2026-04-29, LM Studio + ollama)
|
|
613
|
+
|
|
614
|
+
```
|
|
615
|
+
E-2 (warn): tool-loop-detected ... action: "warn" → 200 OK + provider 応答
|
|
616
|
+
E-3 (inject): tool-loop-detected ... action: "inject" → system に hint 追加 + 200 OK
|
|
617
|
+
+ cache_read_input_tokens: 453 (prefix キャッシュ命中)
|
|
618
|
+
E-4 (break, non-stream): 400 + {"detail":{"error":"tool_loop_detected","profile":"test-loop-break",
|
|
619
|
+
"tool_name":"Read","repeat_count":3,...}}
|
|
620
|
+
E-4 (break, stream): 200 + event: error
|
|
621
|
+
data: {"type":"error","error":{"type":"invalid_request_error",
|
|
622
|
+
"tool_loop":{"profile":"test-loop-break","repeat_count":3,...}}}
|
|
623
|
+
|
|
624
|
+
C (adaptive, 静止): 全 provider 同速 → static order 維持、`adaptive-routing-applied` 出ない
|
|
625
|
+
C (adaptive, 発火): サイズ差 chain (lmstudio 27B-dense 474ms / ollama qwen-coder-1.5b 134ms / openrouter-free n/a)
|
|
626
|
+
→ global_median 304ms × 1.5 = 456ms、lmstudio 474ms ≥ 456ms → demote +1
|
|
627
|
+
→ effective_order: [ollama-qwen-coder-1_5b, openrouter-free, lmstudio-...]
|
|
628
|
+
→ 試験 4 回目から ollama-qwen-coder-1_5b 行きに切り替わって着地、
|
|
629
|
+
debounce 30s で oscillation も観察されず
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
E-2/E-3 は a3 で観察済み、E-4 (両形式) と C 発火パスは GA 直前に実機で初観察。verification.md には MoE モデルの罠 (Qwen3.6-35B-A3B は active 3.8B で速い) と rolling-window タイミング制約の注意を後追いで加筆予定 (本リリースには含まず)。
|
|
633
|
+
|
|
634
|
+
### Migration
|
|
635
|
+
|
|
636
|
+
不要。**v1.8.x / v1.9.0a* からの自然なアップグレード**:
|
|
637
|
+
|
|
638
|
+
- `coderouter` コマンド名 / Python import 名 / providers.yaml の format / env 変数 / ingress URL すべて完全に同じ
|
|
639
|
+
- `tool_loop_action` を未指定または `warn` / `inject` で運用していた profile は挙動完全変化なし
|
|
640
|
+
- `tool_loop_action: break` を既に使っていた profile のみ status code が 5xx → 4xx に変化 (a3〜a6 では実装バグで 500 Internal Server Error が返っていた、1.9.0 で docstring が約束する 400 + 構造化 detail に修正)。実運用で `break` を本番投入していたケースは想定されにくく、検証用途であれば修正後の方が期待挙動
|
|
641
|
+
|
|
642
|
+
### Out of scope (v1.10 以降)
|
|
643
|
+
|
|
644
|
+
v1.9 series は意図的に閉じる:
|
|
645
|
+
|
|
646
|
+
- **v1.9-B2** — `message_delta` event の usage 集約で、streaming 経路でも実 token 数 / cache_read / cache_creation を取得 (現状は `outcome=unknown` 固定)
|
|
647
|
+
- **v1.9-E phase 2** — L2 Memory pressure (LM Studio / ollama backend OOM 検知) / L5 Backend health (continuous probe + chain reorder)
|
|
648
|
+
- **v1.10-?** — plan.md §13 系 (multi-tenant routing, etc.) — 別 minor
|
|
649
|
+
|
|
650
|
+
### Files touched
|
|
651
|
+
|
|
652
|
+
```
|
|
653
|
+
M CHANGELOG.md
|
|
654
|
+
M coderouter/guards/tool_loop.py
|
|
655
|
+
M coderouter/ingress/anthropic_routes.py
|
|
656
|
+
M coderouter/routing/fallback.py
|
|
657
|
+
M pyproject.toml
|
|
658
|
+
M tests/test_ingress_anthropic.py
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## [v1.9.0a6] — 2026-04-28 (v1.9-A streaming パスの cache-observed emit 漏れ patch)
|
|
664
|
+
|
|
665
|
+
**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 で約束していた動作に実装を揃える。
|
|
666
|
+
|
|
667
|
+
- Tests: 826 → **828** (+2: streaming 成功時 emit / streaming 失敗時 emit せず)
|
|
668
|
+
- Runtime deps: 5 → 5 (28 sub-release 連続据え置き)
|
|
669
|
+
- Backward compat: 完全互換、profile / API 全部変更なし
|
|
670
|
+
- Pre-release: `1.9.0a6`
|
|
671
|
+
|
|
672
|
+
### Changes
|
|
673
|
+
|
|
674
|
+
#### `coderouter/routing/fallback.py` `stream_anthropic` に cache-observed emit を追加
|
|
675
|
+
|
|
676
|
+
- `_apply_tool_loop_guard` 直後に `request_had_cache_control = anthropic_request_has_cache_control(request)` を変数化 (v0.5-B の inline call と新規 emit 用 caller の二重評価を回避)
|
|
677
|
+
- successful stream の最後 (`async for ev in event_iter` 完走後、`return` の直前) に `log_cache_observed(...)` を呼ぶ
|
|
678
|
+
- `outcome="unknown"` (v1.9-B が `message_delta` 集約するまで streaming は usage 取得しない約束)
|
|
679
|
+
- `streaming=True`
|
|
680
|
+
- tokens は all 0 (engine は streaming 経路の usage を集約していない、cost も 0)
|
|
681
|
+
- 非 streaming `generate_anthropic` の挙動には影響なし
|
|
682
|
+
|
|
683
|
+
#### Tests
|
|
684
|
+
|
|
685
|
+
- **`tests/test_fallback_cache_observed.py`** + 2:
|
|
686
|
+
- `test_cache_observed_fires_on_streaming_with_unknown_outcome` — 成功 streaming で `outcome=unknown` / `streaming=True` / `request_had_cache_control=True` が記録される
|
|
687
|
+
- `test_cache_observed_streaming_does_not_fire_on_provider_failure` — provider 失敗時は emit しない (非 streaming と同じ contract)
|
|
688
|
+
- 上記のため `_CacheAnthropicAdapter.stream_anthropic` を `NotImplementedError` raise から「3 events (start / delta / stop) を yield する minimal stream」に拡張
|
|
689
|
+
|
|
690
|
+
### Why
|
|
691
|
+
|
|
692
|
+
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 を入れ忘れていた。
|
|
693
|
+
|
|
694
|
+
これは **doc-implementation gap**: dashboard / metrics dashboard 利用者から見ると「streaming で動いているはずなのに observation が記録されない」という不整合になる。v1.9.0a6 は約束と実装を揃える小 patch。
|
|
695
|
+
|
|
696
|
+
副次的効果として A-3 (`hit_rate=null when only `unknown` observations`) の実機検証もこの patch で初めて可能になった。
|
|
697
|
+
|
|
698
|
+
### Migration
|
|
699
|
+
|
|
700
|
+
`pyproject.toml version 1.9.0a5 → 1.9.0a6`、`coderouter --version` は 1.9.0a6 を返す。**手元の `~/.coderouter/providers.yaml` は触らない限り完全に変化なし**。Streaming 経路のレスポンス内容も変化なし — log line が 1 件追加されるだけ。
|
|
701
|
+
|
|
702
|
+
### Files touched
|
|
703
|
+
|
|
704
|
+
```
|
|
705
|
+
M CHANGELOG.md
|
|
706
|
+
M coderouter/routing/fallback.py
|
|
707
|
+
M pyproject.toml
|
|
708
|
+
M tests/test_fallback_cache_observed.py
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### Out of scope (v1.9-B 送り)
|
|
712
|
+
|
|
713
|
+
- `message_delta` event aggregation で streaming 時にも実 token 数 / cache_read / cache_creation を取得する → outcome を unknown 固定でなく実値で出せるようにする
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
## [v1.9.0a5] — 2026-04-28 (v1.9-D: Cost-aware Dashboard — Anthropic prompt-cache aware)
|
|
718
|
+
|
|
719
|
+
**Theme: 「いくら使ってる」を可視化、cache savings を別枠で。** v1.9-A で観測、v1.9-B で透過保証、v1.9-D で **金額に翻訳**。Anthropic の prompt-cache 価格モデル (cache_read 90% 割引、cache_creation 25% 増し) を最初から正確に実装、LiteLLM 競合品が **cache savings を別計算しない** 弱点を構造的にカバー。
|
|
720
|
+
|
|
721
|
+
`docs/inside/future.md` §5.5 の v1.9-D 範囲を実装。
|
|
722
|
+
|
|
723
|
+
- Tests: 811 → **826** (+15: pure compute_cost 8 / collector dispatch 4 / Prometheus exposition 3)
|
|
724
|
+
- Runtime deps: 5 → 5 (27 sub-release 連続据え置き)
|
|
725
|
+
- Backward compat: 完全互換、`providers.yaml` の `cost:` フィールドは optional (unset = 0 contribution)
|
|
726
|
+
- Pre-release: `1.9.0a5`
|
|
727
|
+
|
|
728
|
+
### Changes
|
|
729
|
+
|
|
730
|
+
#### `coderouter/cost.py` 新規 (~150 LOC)
|
|
731
|
+
|
|
732
|
+
- `CostBreakdown` dataclass — per-attempt cost components (input/output/cache_read/cache_creation USD + total + savings)
|
|
733
|
+
- `compute_cost_for_attempt(cost_config, *, input_tokens, ..., cache_creation)` 純関数:
|
|
734
|
+
- 4 token bucket をそれぞれの rate で計算
|
|
735
|
+
- cache_read tokens を `input_rate × cache_read_discount` で割引
|
|
736
|
+
- cache_creation tokens を `input_rate × cache_creation_premium` で premium
|
|
737
|
+
- savings = `cache_read tokens × input_rate × (1 - cache_read_discount)` (cache_creation は premium なので savings には入らない)
|
|
738
|
+
- 負の token / None config / partial config に対する defensive 処理
|
|
739
|
+
|
|
740
|
+
#### Schema: `CostConfig` 新設
|
|
741
|
+
|
|
742
|
+
- **`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
|
|
743
|
+
- `ProviderConfig.cost: CostConfig | None = None` 追加 — opt-in、unset の provider (local 等) は dashboard に 0 contribution
|
|
744
|
+
|
|
745
|
+
#### Engine integration
|
|
746
|
+
|
|
747
|
+
- **`coderouter/routing/fallback.py`**: `_emit_cache_observed` を拡張、`provider_config: ProviderConfig | None = None` パラメータを受けて `compute_cost_for_attempt()` で per-attempt USD cost + savings を計算、log payload に折り込む
|
|
748
|
+
- `generate_anthropic` の call site で `adapter.config` を渡す
|
|
749
|
+
|
|
750
|
+
#### Logging schema 拡張
|
|
751
|
+
|
|
752
|
+
- **`coderouter/logging.py`** `CacheObservedPayload` に `cost_usd: float` / `cost_savings_usd: float` フィールド追加 (default 0.0、pre-v1.9-D caller は zero contribution で互換)
|
|
753
|
+
- `log_cache_observed` helper の signature にも optional kwargs 追加
|
|
754
|
+
|
|
755
|
+
#### MetricsCollector: per-provider cost aggregation
|
|
756
|
+
|
|
757
|
+
- **`coderouter/metrics/collector.py`**: `cache-observed` event の dispatch で cost を集計
|
|
758
|
+
- `_cost_total_usd: dict[str, float]` (per-provider)
|
|
759
|
+
- `_cost_savings_usd: dict[str, float]` (per-provider)
|
|
760
|
+
- `_cost_total_usd_aggregate: float` / `_cost_savings_usd_aggregate: float` (process-wide)
|
|
761
|
+
- `snapshot()` 拡張:
|
|
762
|
+
- `counters.cost_total_usd` / `cost_savings_usd` (per-provider dict)
|
|
763
|
+
- `counters.cost_total_usd_aggregate` / `cost_savings_usd_aggregate` (process-wide)
|
|
764
|
+
- 各 provider row に `cost: {total_usd, savings_usd}` panel
|
|
765
|
+
- `reset()` で v1.9-D state も clear
|
|
766
|
+
- 防御的: malformed cost values (str/None) → 0.0 default、handler は raise しない
|
|
767
|
+
|
|
768
|
+
#### Prometheus exposition
|
|
769
|
+
|
|
770
|
+
- **`coderouter/metrics/prometheus.py`**: 新 helper `_counter_float()` (float-valued counter、`.10g` formatter で trailing zero trim) + 2 つの新 metric:
|
|
771
|
+
- `coderouter_cost_total_usd_total{provider}` — cumulative USD billed
|
|
772
|
+
- `coderouter_cost_savings_usd_total{provider}` — cumulative cache savings USD
|
|
773
|
+
|
|
774
|
+
#### Tests (+15)
|
|
775
|
+
|
|
776
|
+
- **`tests/test_metrics_cost.py`** 新規:
|
|
777
|
+
- `compute_cost_for_attempt`: None config / no cache / cache read discount / cache creation premium / combined / negative tokens defensive / partial config (7)
|
|
778
|
+
- Collector dispatch: per-provider aggregation / zero cost no entry / per-row cost panel / reset / malformed values (5)
|
|
779
|
+
- Prometheus: HELP+TYPE / per-provider labels / `_total` suffix (3)
|
|
780
|
+
|
|
781
|
+
### Why
|
|
782
|
+
|
|
783
|
+
`docs/inside/future.md` §5.5 で確立した「LiteLLM ですら未対応の cache savings 計算を最初から正確に実装」の具体実装。Anthropic 価格モデルを 4 token bucket × 4 multiplier で正確に表現、operator が「ローカル LLM 併用でいくら浮いたか」「Anthropic prompt cache でいくら節約できたか」を 1 画面で見える状態を実現。
|
|
784
|
+
|
|
785
|
+
**競合状況**:
|
|
786
|
+
- LiteLLM の cost tracker は `cache_read_input_tokens` を full input rate で billing (= overstate)、savings 別計算なし
|
|
787
|
+
- claude-code-router は cost tracking 自体なし
|
|
788
|
+
- v1.9-D は **Claude Code 系 OSS で唯一、cache-aware cost dashboard を持つ**
|
|
789
|
+
|
|
790
|
+
### Migration
|
|
791
|
+
|
|
792
|
+
`pyproject.toml version 1.9.0a4 → 1.9.0a5`、`coderouter --version` は 1.9.0a5 を返す。**手元の `~/.coderouter/providers.yaml` は触らない限り完全に変化なし**。
|
|
793
|
+
|
|
794
|
+
明示的に有効化する operator は paid provider に `cost:` ブロックを追加:
|
|
795
|
+
|
|
796
|
+
```yaml
|
|
797
|
+
providers:
|
|
798
|
+
- name: anthropic-direct
|
|
799
|
+
kind: anthropic
|
|
800
|
+
base_url: https://api.anthropic.com
|
|
801
|
+
model: claude-sonnet-4-8
|
|
802
|
+
api_key_env: ANTHROPIC_API_KEY
|
|
803
|
+
paid: true
|
|
804
|
+
cost: # v1.9-D 新フィールド
|
|
805
|
+
input_tokens_per_million: 3.00
|
|
806
|
+
output_tokens_per_million: 15.00
|
|
807
|
+
cache_read_discount: 0.10 # default、省略可
|
|
808
|
+
cache_creation_premium: 1.25 # default、省略可
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
`coderouter serve` 起動後、`/metrics.json` の `counters.cost_total_usd` / `cost_savings_usd` で per-provider cost を取得可能。Prometheus scrape は `coderouter_cost_total_usd_total{provider="anthropic-direct"}` で取れる。
|
|
812
|
+
|
|
813
|
+
### Files touched
|
|
814
|
+
|
|
815
|
+
```
|
|
816
|
+
M CHANGELOG.md
|
|
817
|
+
M coderouter/config/schemas.py
|
|
818
|
+
M coderouter/logging.py
|
|
819
|
+
M coderouter/metrics/collector.py
|
|
820
|
+
M coderouter/metrics/prometheus.py
|
|
821
|
+
M coderouter/routing/fallback.py
|
|
822
|
+
M pyproject.toml
|
|
823
|
+
A coderouter/cost.py
|
|
824
|
+
A tests/test_metrics_cost.py
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
### Out of scope (次回以降)
|
|
828
|
+
|
|
829
|
+
- **`/dashboard` HTML cost panel**: snapshot schema は揃ったが UI 描画は v1.9-D2 で
|
|
830
|
+
- **`coderouter stats --cost` TUI**: 5 行サマリ CLI コマンドは v1.9-D2 で
|
|
831
|
+
- **期間別累積 (1 day / 1 week / 1 month)**: 現在 process-lifetime のみ。期間集計は SQLite persistence と組み合わせて v1.10 候補
|
|
832
|
+
- **OpenAI-shaped engine paths のコスト集計**: Anthropic 非 streaming 経路のみ。OpenAI ingress + streaming 対応は v1.9-C2 と同じ follow-up
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
|
|
836
|
+
## [v1.9.0a4] — 2026-04-28 (v1.9-C: Adaptive Routing — health-based dynamic chain priority)
|
|
837
|
+
|
|
838
|
+
**Theme: 「平常時の最適化」を chain に持ち込む。** 静的に declare した `providers` 順序を、live observed の median latency / error rate に基づいて自動再優先化。L5 (v1.9-E phase 3 予定) は二値 (HEALTHY/UNHEALTHY) で crash 対応するのに対し、C は連続値 gradient で **平常時の遅さ** を吸収する。両方とも同じ observation stream から動くが、適用ロジックが直交。
|
|
839
|
+
|
|
840
|
+
`docs/inside/future.md` §5.4 の v1.9-C 範囲を MVP 実装。**Anthropic 非 streaming パスのみ** 対応 (v1.9-C2 で OpenAI-shaped + streaming follow-up 予定)。
|
|
841
|
+
|
|
842
|
+
- Tests: 795 → **811** (+16: stats 4 / no-demote 3 / latency demote 2 / error-rate demote 2 / debounce 2 / engine integration 2 / constants pin 1)
|
|
843
|
+
- Runtime deps: 5 → 5 (26 sub-release 連続据え置き)
|
|
844
|
+
- Backward compat: 完全互換、既存 profile は default の `adaptive: false` で従来挙動を維持
|
|
845
|
+
- Pre-release: `1.9.0a4`、`pip install --pre coderouter-cli` で取得可能
|
|
846
|
+
|
|
847
|
+
### Changes
|
|
848
|
+
|
|
849
|
+
#### `coderouter/routing/adaptive.py` 新規 (~360 LOC)
|
|
850
|
+
|
|
851
|
+
- `AdaptiveAdjuster` クラス — per-process singleton (engine が 1 つ保持)
|
|
852
|
+
- `record_attempt(provider, *, latency_ms, success, now=None)` — observation 記録、append on each engine attempt
|
|
853
|
+
- `stats_for(provider, *, now=None) -> ProviderStats` — rolling-window から median latency + error rate 計算
|
|
854
|
+
- `compute_effective_order(adapters, *, now=None) -> list[BaseAdapter]` — 静的 chain → 動的順序、debounce 適用
|
|
855
|
+
- `_ProviderObservation` / `_AdjusterState` / `ProviderStats` データクラス
|
|
856
|
+
- `_apply_debounce` 内部メソッド — `last_committed_rank` 比較で debounce window 内の rank 変更を pinning (両方向、demote→promote と promote→demote 両方)
|
|
857
|
+
- 定数:
|
|
858
|
+
- `ROLLING_WINDOW_S = 60.0`
|
|
859
|
+
- `LATENCY_DEMOTE_FACTOR = 1.5` (median × 1.5 を超えたら -1 段)
|
|
860
|
+
- `ERROR_RATE_DEMOTE_THRESHOLD = 0.10` (10% 失敗で -2 段)
|
|
861
|
+
- `DEBOUNCE_S = 30.0`
|
|
862
|
+
- `MIN_SAMPLES_FOR_LATENCY = 3` / `MIN_SAMPLES_FOR_ERROR_RATE = 5`
|
|
863
|
+
|
|
864
|
+
#### Engine integration (`coderouter/routing/fallback.py`)
|
|
865
|
+
|
|
866
|
+
- `FallbackEngine.__init__` で `_adaptive_adjuster: AdaptiveAdjuster` を eager 構築。`@property` の `_adaptive` で lazy-fallback も用意 (legacy test `__new__` bypass パターンに対する resilience)
|
|
867
|
+
- `_resolve_anthropic_chain`: profile が `adaptive: true` のときに `_adaptive.compute_effective_order(base)` で chain を再優先化、その後 thinking-capable bucket logic に渡す
|
|
868
|
+
- `_profile_is_adaptive(profile_name)` ヘルパ — chain resolver と recording 側で同じ profile lookup を共有
|
|
869
|
+
- `generate_anthropic` の adapter 呼び出しを `time.monotonic()` で wrap、success/failure 両方で `record_attempt(...)` 呼び出し。auth-flavored failures (401/403) は latency_ms=None で記録 (短絡応答なので latency 信号として無意味)
|
|
870
|
+
|
|
871
|
+
#### Logging
|
|
872
|
+
|
|
873
|
+
- 新 event `adaptive-routing-applied` (info-level) — 静的 chain と effective chain order が異なるときのみ fire。payload に static_order / effective_order / per-provider stats を含む
|
|
874
|
+
|
|
875
|
+
#### Config schema
|
|
876
|
+
|
|
877
|
+
- `FallbackChain.adaptive: bool = False` 追加。既存 yaml はそのまま動く (default false)
|
|
878
|
+
|
|
879
|
+
#### Tests
|
|
880
|
+
|
|
881
|
+
- **`tests/test_routing_adaptive.py`** 新規 (+16 tests):
|
|
882
|
+
- **Stats**: unseen / median は success のみ / window roll-off / error rate zero on empty (4)
|
|
883
|
+
- **No demote**: empty chain / no obs / all fast (3)
|
|
884
|
+
- **Latency demote**: 1.5× threshold / min samples gate (2)
|
|
885
|
+
- **Error rate demote**: 10% threshold / min samples gate (2)
|
|
886
|
+
- **Debounce**: pin within window / release after window (2)
|
|
887
|
+
- **Engine integration**: static profile not invoking adjuster / adaptive profile invoking adjuster (2)
|
|
888
|
+
- **Constants pin**: ROLLING_WINDOW_S / LATENCY_DEMOTE_FACTOR / ERROR_RATE_DEMOTE_THRESHOLD / DEBOUNCE_S / MIN_SAMPLES_* (1)
|
|
889
|
+
|
|
890
|
+
### Why
|
|
891
|
+
|
|
892
|
+
`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 に追従するようになり、両軸が初めて補完関係を成す。
|
|
893
|
+
|
|
894
|
+
**競合状況**: claude-code-router は task-based 単独、LiteLLM は session-cost-based、何れも latency-aware adaptive routing を持たない。CodeRouter は v1.9-C で **task-based + health-based 両軸** を持つ唯一の Claude Code 系 OSS という位置づけ。
|
|
895
|
+
|
|
896
|
+
### Migration
|
|
897
|
+
|
|
898
|
+
`pyproject.toml version 1.9.0a3 → 1.9.0a4`、`coderouter --version` は 1.9.0a4 を返す。**手元の `~/.coderouter/providers.yaml` は触らない限り完全に変化なし**。新フィールド `adaptive: false` がデフォルトなので、既存 profile はゼロ変更で従来動作を維持。
|
|
899
|
+
|
|
900
|
+
明示的に有効化する operator は profile に追加:
|
|
901
|
+
|
|
902
|
+
```yaml
|
|
903
|
+
profiles:
|
|
904
|
+
- name: coding
|
|
905
|
+
providers:
|
|
906
|
+
- lmstudio-qwen3-5-9b
|
|
907
|
+
- ollama-gemma4-26b
|
|
908
|
+
- openrouter-free
|
|
909
|
+
adaptive: true # 平常時の latency / error rate に基づく動的優先度
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
### Files touched
|
|
913
|
+
|
|
914
|
+
```
|
|
915
|
+
M CHANGELOG.md
|
|
916
|
+
M coderouter/config/schemas.py
|
|
917
|
+
M coderouter/routing/fallback.py
|
|
918
|
+
M pyproject.toml
|
|
919
|
+
A coderouter/routing/adaptive.py
|
|
920
|
+
A tests/test_routing_adaptive.py
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
### Out of scope (次回以降の v1.9-C2)
|
|
924
|
+
|
|
925
|
+
- **OpenAI-shaped engine paths**: `generate` / `stream` (非 Anthropic ingress) からの `record_attempt` 呼び出し。MVP では Anthropic 非 streaming のみカバー
|
|
926
|
+
- **Anthropic streaming**: `stream_anthropic` の latency 計測 (mid-stream success の境界をどこに置くか設計余地あり)
|
|
927
|
+
- **Dashboard panel**: `/dashboard` に effective chain order の可視化 (「static order vs current effective order」の差分強調表示)
|
|
928
|
+
- **MetricsCollector への adaptive 集計**: 現在は `adaptive-routing-applied` log のみ。将来 dashboard panel 用に reorder 回数 / 直近 reorder timestamp などを集計
|
|
929
|
+
- **L5 (v1.9-E phase 3)**: binary HEALTHY/UNHEALTHY backend swap。本実装の continuous gradient と棲み分け、両方とも同じ observation stream を消費する設計
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
## [v1.9.0a3] — 2026-04-28 (v1.9-E phase 1: L3 Tool-loop detection guard)
|
|
934
|
+
|
|
935
|
+
**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 を切る。
|
|
936
|
+
|
|
937
|
+
L3 は最も isolated で HTTP 系の依存なし、~300 LOC、self-contained。「Claude Code を 8 時間連続で local LLM に向けて使っても止まらない」を訴求するための最初の具体実装。
|
|
938
|
+
|
|
939
|
+
- Tests: 779 → **795** (+16: pure detect 8 / inject mutation 3 / engine helper 5)
|
|
940
|
+
- Runtime deps: 5 → 5 (25 sub-release 連続据え置き)
|
|
941
|
+
- Backward compat: 完全互換、`providers.yaml` 編集不要 (新フィールドはすべて default 値あり)
|
|
942
|
+
- Pre-release: `1.9.0a3`、`pip install --pre coderouter-cli` で取得可能
|
|
943
|
+
|
|
944
|
+
### Changes
|
|
945
|
+
|
|
946
|
+
#### `coderouter/guards/` 新パッケージ + L3 detector
|
|
947
|
+
|
|
948
|
+
- **`coderouter/guards/__init__.py`** 新規 — Long-run guards のパッケージドッジ。L2 / L5 が今後追加される予定地。
|
|
949
|
+
- **`coderouter/guards/tool_loop.py`** 新規 (~250 LOC):
|
|
950
|
+
- `detect_tool_loop(request, *, window, threshold) -> ToolLoopDetection | None` 純関数。直近 `window` 件の assistant `tool_use` ブロックの**末尾連続**で同一 `(name, args)` が `threshold` 回以上発生していると検知
|
|
951
|
+
- `ToolUseRecord` / `ToolLoopDetection` データクラス
|
|
952
|
+
- `inject_loop_break_hint(request, *, hint)` — system フィールドに hint を append (str / None / list-of-blocks の 3 形を吸収)
|
|
953
|
+
- `ToolLoopBreakError` (CodeRouterError 派生) — `break` action 用 exception
|
|
954
|
+
- `DEFAULT_LOOP_INJECT_HINT` 定数 — 「You appear to be calling the same tool with the same arguments repeatedly...」
|
|
955
|
+
- **canonical-form JSON 比較** (`json.dumps(args, sort_keys=True)`) で `{"a":1,"b":2}` と `{"b":2,"a":1}` を同一視
|
|
956
|
+
- **trailing-run only** 検出 — 過去に脱出済みの streak は無視 (現在状態のみが actionable)
|
|
957
|
+
|
|
958
|
+
#### Engine integration
|
|
959
|
+
|
|
960
|
+
- **`coderouter/routing/fallback.py`**: `_apply_tool_loop_guard(request, config)` ヘルパ追加。`generate_anthropic` / `stream_anthropic` の chain dispatch 直前で呼ばれる。Action 別の挙動:
|
|
961
|
+
- `warn`: log のみ、request はそのまま
|
|
962
|
+
- `inject`: log + `inject_loop_break_hint` で system 注入された新 request を返す
|
|
963
|
+
- `break`: log + `raise ToolLoopBreakError`
|
|
964
|
+
- profile lookup 失敗時は silent no-op (chain resolution が別経路で error を出すので二重診断にならない)
|
|
965
|
+
|
|
966
|
+
#### Config schema
|
|
967
|
+
|
|
968
|
+
- **`coderouter/config/schemas.py`** `FallbackChain` 拡張:
|
|
969
|
+
- `tool_loop_window: int = 5` (range 2-50)
|
|
970
|
+
- `tool_loop_threshold: int = 3` (range 2-50)
|
|
971
|
+
- `tool_loop_action: Literal["warn", "inject", "break"] = "warn"`
|
|
972
|
+
- 既存 profile はすべて default で warn-only として動作 → 既存 deployment はゼロ変更
|
|
973
|
+
|
|
974
|
+
#### Logging
|
|
975
|
+
|
|
976
|
+
- **`coderouter/logging.py`**: `tool-loop-detected` warn-level log shape を新設
|
|
977
|
+
- `ToolLoopDetectedPayload` TypedDict (profile / tool_name / repeat_count / threshold / window / action)
|
|
978
|
+
- `log_tool_loop_detected()` helper — 単一の chokepoint
|
|
979
|
+
- 3 つの action すべてが同じ log line を fire するので dashboard は detection 全件を捕捉できる (action は label として区別)
|
|
980
|
+
|
|
981
|
+
### Why
|
|
982
|
+
|
|
983
|
+
`docs/inside/future.md` §1 で確立した Vision「Local LLM で agent を長時間回すための信頼性層」の P3 (Long-run Reliability) の最初の具体実装。L3 が最も isolated で実装シンプル / テスト容易 / 単独で価値があり、最初の sub-release に最適。
|
|
984
|
+
|
|
985
|
+
「Claude Code が同じファイルを 5 回 Read し続ける」「Bash で同じコマンドを 3 回叩いて止まらない」というのは長時間 agent loop で頻出する典型症状で、L3 はその検知を request shape だけで完結させる (Claude Code は full conversation history を毎回送るので tail inspection で十分)。
|
|
986
|
+
|
|
987
|
+
**競合状況** (future.md §3 referenced): L3 を体系的に対処する Claude Code 系 OSS は 2026-04-27 時点で調査リスト中ゼロ。本実装は単独差別化軸として位置づく。
|
|
988
|
+
|
|
989
|
+
### Migration
|
|
990
|
+
|
|
991
|
+
`pyproject.toml version 1.9.0a2 → 1.9.0a3`、`coderouter --version` は 1.9.0a3 を返す。**手元の `~/.coderouter/providers.yaml` は触らない限り完全に変化なし**。新 schema フィールドはすべて default 値ありなので、既存 yaml はそのままロード可能で、警告の挙動も warn level (ログ出力のみ) なので既存処理に副作用なし。
|
|
992
|
+
|
|
993
|
+
明示的に有効化したい operator は profile に以下を追加:
|
|
994
|
+
|
|
995
|
+
```yaml
|
|
996
|
+
profiles:
|
|
997
|
+
- name: long-running-agent
|
|
998
|
+
providers: [...]
|
|
999
|
+
tool_loop_window: 5
|
|
1000
|
+
tool_loop_threshold: 3
|
|
1001
|
+
tool_loop_action: inject # または warn / break
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### Files touched
|
|
1005
|
+
|
|
1006
|
+
```
|
|
1007
|
+
M CHANGELOG.md
|
|
1008
|
+
M coderouter/config/schemas.py
|
|
1009
|
+
M coderouter/logging.py
|
|
1010
|
+
M coderouter/routing/fallback.py
|
|
1011
|
+
M pyproject.toml
|
|
1012
|
+
A coderouter/guards/__init__.py
|
|
1013
|
+
A coderouter/guards/tool_loop.py
|
|
1014
|
+
A tests/test_guards_tool_loop.py
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
### Out of scope (次回以降の v1.9-E phase)
|
|
1018
|
+
|
|
1019
|
+
- **L2 (Memory pressure awareness)**: Ollama `/api/ps` / LM Studio `/v1/models` / llama.cpp `/proc/meminfo` 直読みで backend memory probe、95% 超で軽量 model に swap
|
|
1020
|
+
- **L5 (Backend health continuous monitoring)**: 60s 周期の健康 probe、UNHEALTHY を chain 末尾に降格 / 復帰時に元 priority 戻し、dashboard に effective chain order
|
|
1021
|
+
- **MetricsCollector への loop event 集計**: 現在は構造化 log のみ、将来 dashboard panel で「直近 24h の loop 検知 N 件」表示
|
|
1022
|
+
- **inject hint の operator override**: 現在 `DEFAULT_LOOP_INJECT_HINT` のみ、将来 profile-level `tool_loop_inject_hint` で日本語化等可能に
|
|
1023
|
+
|
|
1024
|
+
---
|
|
1025
|
+
|
|
1026
|
+
## [v1.9.0a2] — 2026-04-28 (v1.9-B: Cross-backend cache passthrough + capability gate + doctor cache probe)
|
|
1027
|
+
|
|
1028
|
+
**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。
|
|
1029
|
+
|
|
1030
|
+
`docs/inside/future.md` §5.2 の v1.9-B 範囲を実装。挙動変更は capability gate 拡張のみで、既存の `provider_supports_cache_control` 呼び出しは下位互換 (registry 未宣言 anthropic-kind は引き続き True)。
|
|
1031
|
+
|
|
1032
|
+
- Tests: 759 → **779** (+20: registry resolution 12 / doctor cache probe 8)
|
|
1033
|
+
- Runtime deps: 5 → 5 (24 sub-release 連続据え置き)
|
|
1034
|
+
- Backward compat: 完全互換、`providers.yaml` / API 全部変更なし
|
|
1035
|
+
- Pre-release: `1.9.0a2`、`pip install --pre coderouter-cli` で取得可能
|
|
1036
|
+
|
|
1037
|
+
### Changes
|
|
1038
|
+
|
|
1039
|
+
#### Capability registry: `cache_control` フィールド新設
|
|
1040
|
+
|
|
1041
|
+
- **`coderouter/config/capability_registry.py`**: `RegistryCapabilities` / `ResolvedCapabilities` に `cache_control: bool | None` フィールド追加。lookup walker に同フィールドを追加 (first-match-per-flag 既存 semantics に従う)。
|
|
1042
|
+
- **`coderouter/data/model-capabilities.yaml`**: bundled で 5 rule 宣言:
|
|
1043
|
+
- `claude-opus-4-*` / `claude-sonnet-4-*` / `claude-haiku-4-*` (kind=anthropic): `cache_control: true` — api.anthropic.com で実機検証済 (2026-04-20、1321 tokens 書き / 1321 tokens 読み)
|
|
1044
|
+
- `qwen3.5-*` / `qwen3.6-*` (kind=anthropic): `cache_control: true` — LM Studio 0.4.12 `/v1/messages` で v1.8.4 実機検証済 (`cache_read_input_tokens: 280` 観測)
|
|
1045
|
+
- openai_compat 系は意図的に未宣言 (= None) → 既存の v0.5-B `capability-degraded reason=translation-lossy` log がそのまま fire
|
|
1046
|
+
|
|
1047
|
+
#### Capability gate: registry を consult
|
|
1048
|
+
|
|
1049
|
+
- **`coderouter/routing/capability.py`**: `provider_supports_cache_control` に `registry: CapabilityRegistry | None = None` kwarg を追加。解決順序を 3 段に:
|
|
1050
|
+
1. `provider.capabilities.prompt_cache: true` → True (explicit per-provider)
|
|
1051
|
+
2. registry の `cache_control: true|false` → 即決
|
|
1052
|
+
3. fallback: `provider.kind == "anthropic"` → True (pre-v1.9-B 互換)
|
|
1053
|
+
- registry が `False` を返したら kind=anthropic でも False を返すので、upstream regression 時に operator が一時的に `cache_control: false` を user yaml で declare → `capability-degraded` log が fire するという escape hatch が成立
|
|
1054
|
+
|
|
1055
|
+
#### Doctor: `_probe_cache` 新 probe 追加
|
|
1056
|
+
|
|
1057
|
+
- **`coderouter/doctor.py`**: `_probe_cache` 関数を新設、orchestrator の最後 (streaming probe の後) に組み込み。auth fail 時の SKIP list にも追加。
|
|
1058
|
+
- 動作: 同一 body (~1900 token system prompt + `cache_control: ephemeral`) を 2 回 POST、1 回目で `cache_creation_input_tokens > 0`、2 回目で `cache_read_input_tokens > 0` を期待
|
|
1059
|
+
- **Verdict 4 種**:
|
|
1060
|
+
- **OK**: 2 回目で read > 0 → cache_control 配管が end-to-end 機能している
|
|
1061
|
+
- **NEEDS_TUNING**: 1 回目 creation 観測 / 2 回目 read=0 → TTL 短すぎ or cache key mismatch
|
|
1062
|
+
- **NEEDS_TUNING**: 両方とも creation/read 観測なし → upstream が cache_control を silent ignore (Anthropic compat 不完全) or 1024 token 最低未達
|
|
1063
|
+
- **SKIP**: not anthropic / 未宣言 / upstream 5xx / auth fail
|
|
1064
|
+
- **Gate は意図的に tight**: 2 paid HTTP call を消費するので、registry に `cache_control: true` 明示宣言 OR `providers.yaml capabilities.prompt_cache: true` のときのみ実行。kind=anthropic だけで自動実行はしない (unverified model に対して無駄な call を避ける)
|
|
1065
|
+
|
|
1066
|
+
#### Tests
|
|
1067
|
+
|
|
1068
|
+
- **`tests/test_capability_registry_cache_control.py`** 新規 (+12): registry resolution 4 / capability gate 5 / bundled YAML 検証 3
|
|
1069
|
+
- bundled が `claude-opus-4-8` / `claude-sonnet-4-7` / `claude-haiku-4-1` で `cache_control=true` を返すこと
|
|
1070
|
+
- bundled が `qwen3.5-9b` / `qwen3.6-35b-a3b` で `cache_control=true` を返すこと
|
|
1071
|
+
- bundled が `openai_compat` の `qwen2.5-coder:7b` で undeclared (None) のまま → translation-lossy gate fire を確実にする
|
|
1072
|
+
- **`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
|
|
1073
|
+
|
|
1074
|
+
### Why
|
|
1075
|
+
|
|
1076
|
+
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 コマンドで確認できる単独差別化軸。
|
|
1077
|
+
|
|
1078
|
+
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 実利用可能、という保証関係。
|
|
1079
|
+
|
|
1080
|
+
### Migration
|
|
1081
|
+
|
|
1082
|
+
`pyproject.toml version 1.9.0a1 → 1.9.0a2`、`coderouter --version` は 1.9.0a2 を返す。**手元の `~/.coderouter/providers.yaml` は触らない限り完全に変化なし**。
|
|
1083
|
+
|
|
1084
|
+
`provider_supports_cache_control` は kwarg `registry=None` を追加したので signature は backward-compatible (既存 caller は変更なし)。registry を consult した結果 `False` で hard-disable できるのが新機能だが、bundled YAML は positive 宣言のみ ship なので default 挙動は変化なし。
|
|
1085
|
+
|
|
1086
|
+
### Files touched
|
|
1087
|
+
|
|
1088
|
+
```
|
|
1089
|
+
M CHANGELOG.md
|
|
1090
|
+
M coderouter/config/capability_registry.py
|
|
1091
|
+
M coderouter/data/model-capabilities.yaml
|
|
1092
|
+
M coderouter/doctor.py
|
|
1093
|
+
M coderouter/routing/capability.py
|
|
1094
|
+
M pyproject.toml
|
|
1095
|
+
A tests/test_capability_registry_cache_control.py
|
|
1096
|
+
A tests/test_doctor_cache_probe.py
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
### Out of scope (次回以降)
|
|
1100
|
+
|
|
1101
|
+
- **v1.9-E (前倒し)**: Long-run Guards 三段 (L2 memory pressure / L3 tool loop / L5 backend health continuous) — Vision の核心実装
|
|
1102
|
+
- **v1.9-C**: Adaptive Routing (rolling latency window + health-based dynamic priority)
|
|
1103
|
+
- **v1.9-D**: Cost-aware Dashboard
|
|
1104
|
+
- streaming aggregation: cache 観測の streaming 時 `outcome` 値を `cache_hit/creation/no_cache` に格上げ (v1.9-A の `unknown` から)
|
|
1105
|
+
|
|
1106
|
+
---
|
|
1107
|
+
|
|
1108
|
+
## [v1.9.0a1] — 2026-04-28 (v1.9-A: Cache Observability — Anthropic prompt caching を観測可能に)
|
|
1109
|
+
|
|
1110
|
+
**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 集計。**
|
|
1111
|
+
|
|
1112
|
+
`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 制御を追加予定。
|
|
1113
|
+
|
|
1114
|
+
- Tests: 737 → **759** (+22: classify_cache_outcome / collector dispatch / snapshot cache panel / Prometheus exposition / engine emission)
|
|
1115
|
+
- Runtime deps: 5 → 5 (23 sub-release 連続据え置き)
|
|
1116
|
+
- Backward compat: 完全互換、`providers.yaml` / `~/.coderouter/model-capabilities.yaml` / API 全部変更なし
|
|
1117
|
+
- 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 も完了次第
|
|
1118
|
+
|
|
1119
|
+
### Changes
|
|
1120
|
+
|
|
1121
|
+
#### `cache-observed` 構造化ログイベント新設
|
|
1122
|
+
|
|
1123
|
+
- **`coderouter/logging.py`**: `CacheOutcome` Literal + `CacheObservedPayload` TypedDict + `log_cache_observed()` helper + `classify_cache_outcome()` 4 分類関数を追加。
|
|
1124
|
+
- `cache_hit`: `cache_read_input_tokens > 0` (cache 再利用、〜10% input rate)
|
|
1125
|
+
- `cache_creation`: `cache_creation_input_tokens > 0` かつ hit ではない (cache 書き込み、〜125% input rate)
|
|
1126
|
+
- `no_cache`: usage 受信したが cache フィールド 0/欠損 (cache_control 無し or upstream が握り潰した)
|
|
1127
|
+
- `unknown`: response に usage block 自体無し (streaming / openai_compat 経由 / pre-v1.9-A upstream)
|
|
1128
|
+
- **理由**: `provider-ok` event に cache フィールドを混ぜると downstream consumers (collector / JSONL mirror / tests) すべてが新 schema 検証必要。専用 event なら streaming 時の `outcome=unknown` も自然に表現できる
|
|
1129
|
+
|
|
1130
|
+
#### Engine (`fallback.py`): 成功 response 毎に cache-observed を emit
|
|
1131
|
+
|
|
1132
|
+
- **`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"` 経由でラウンドトリップ済み)。
|
|
1133
|
+
- native Anthropic + LM Studio `/v1/messages` (`kind: anthropic`) → cache フィールド付き → 4 分類正しく出る
|
|
1134
|
+
- openai_compat → anthropic 変換経由 → cache フィールド無し → `outcome=no_cache` or `unknown`
|
|
1135
|
+
- streaming aggregation は v1.9-B 送り (`message_delta` イベント集約が必要)、v1.9-A では非 streaming パスのみ対応
|
|
1136
|
+
|
|
1137
|
+
#### MetricsCollector: per-provider cache 集計
|
|
1138
|
+
|
|
1139
|
+
- **`coderouter/metrics/collector.py`**: `cache-observed` event を dispatch table に追加。新カウンタ:
|
|
1140
|
+
- `_cache_read_tokens: Counter[str]` (per-provider)
|
|
1141
|
+
- `_cache_creation_tokens: Counter[str]` (per-provider)
|
|
1142
|
+
- `_cache_outcomes: dict[str, Counter[str]]` (per-provider × 4-class)
|
|
1143
|
+
- `_cache_read_tokens_total: int` / `_cache_creation_tokens_total: int` (aggregate、毎 event で incremental 更新、snapshot 時の re-fold コスト回避)
|
|
1144
|
+
- `snapshot()` 拡張: `counters.cache_*` (per-provider + aggregate) + 各 provider row に `cache: {read_tokens, creation_tokens, outcomes, hit_rate, observations}` panel を追加
|
|
1145
|
+
- **`hit_rate`** は `cache_hit / (cache_hit + cache_creation + no_cache)`、`unknown` は分母から除外 (signal 無しを 0% 表示するのを回避)
|
|
1146
|
+
- 観測無しなら `hit_rate=None`、dashboard で「—」表示できる
|
|
1147
|
+
- `reset()` で v1.9-A state も clear
|
|
1148
|
+
|
|
1149
|
+
#### Prometheus exposition: 3 つの新 counter
|
|
1150
|
+
|
|
1151
|
+
- **`coderouter/metrics/prometheus.py`**:
|
|
1152
|
+
- `coderouter_cache_read_tokens_total{provider="..."}` — cache 再利用された input token 累計
|
|
1153
|
+
- `coderouter_cache_creation_tokens_total{provider="..."}` — cache 書き込み input token 累計
|
|
1154
|
+
- `coderouter_cache_observed_total{provider="...", outcome="cache_hit|cache_creation|no_cache|unknown"}` — 4 分類イベント数
|
|
1155
|
+
- `hit_rate` を gauge で expose しないのは Prometheus 慣習に従い (`rate()` で derivative を計算する方が時間窓を正しく扱える)
|
|
1156
|
+
|
|
1157
|
+
#### Tests (+22)
|
|
1158
|
+
|
|
1159
|
+
- **`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 受け入れ
|
|
1160
|
+
- **`tests/test_metrics_prometheus_cache.py`** (+5): empty-snapshot HELP/TYPE / per-provider read/creation labels / outcome label pair / `_total` suffix
|
|
1161
|
+
- **`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
|
|
1162
|
+
|
|
1163
|
+
### Why
|
|
1164
|
+
|
|
1165
|
+
`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 率として集計・可視化** できるようになった。
|
|
1166
|
+
|
|
1167
|
+
LiteLLM cluster は `cache_creation_input_tokens` を `no_cache` に丸めて undercount する既知バグ (future.md §3 referenced) があり、CodeRouter は最初から 4 分類厳密集計でこれを回避。Claude Code 特化 OSS の中で **唯一の cache 観測機能** として位置づけ。
|
|
1168
|
+
|
|
1169
|
+
### Migration
|
|
1170
|
+
|
|
1171
|
+
`pyproject.toml version 1.8.5 → 1.9.0a1`、`coderouter --version` は 1.9.0a1 を返す。**手元の `~/.coderouter/providers.yaml` は触らない限り完全に変化なし**。
|
|
1172
|
+
|
|
1173
|
+
`/metrics.json` の counters / providers schema は **追加のみ** (新 key `cache_read_tokens` / `cache_creation_tokens` / `cache_outcomes`、provider rows に `cache` panel)、既存 dashboards は壊れない。Prometheus scraper は新メトリクス自動 discovery。
|
|
1174
|
+
|
|
1175
|
+
### Files touched
|
|
1176
|
+
|
|
1177
|
+
```
|
|
1178
|
+
M CHANGELOG.md
|
|
1179
|
+
M coderouter/logging.py
|
|
1180
|
+
M coderouter/metrics/collector.py
|
|
1181
|
+
M coderouter/metrics/prometheus.py
|
|
1182
|
+
M coderouter/routing/fallback.py
|
|
1183
|
+
M pyproject.toml
|
|
1184
|
+
A tests/test_fallback_cache_observed.py
|
|
1185
|
+
A tests/test_metrics_cache.py
|
|
1186
|
+
A tests/test_metrics_prometheus_cache.py
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
### Out of scope (次回以降)
|
|
1190
|
+
|
|
1191
|
+
- **v1.9-B**: cross-backend cache passthrough + capability gate (`capabilities.cache_control` registry / doctor cache probe / openai_compat strip warn) — 「観測」から「保証」へ
|
|
1192
|
+
- **v1.9-E (前倒し)**: Long-run Guards 三段 (L2 memory pressure / L3 tool loop / L5 backend health) — Vision の核心実装
|
|
1193
|
+
- streaming aggregation: `message_delta` event を集約して streaming 時も `outcome=cache_hit/creation/no_cache` を出せるようにする (v1.9-B 範囲)
|
|
1194
|
+
|
|
1195
|
+
---
|
|
1196
|
+
|
|
9
1197
|
## [v1.8.5] — 2026-04-28 (doctor NEEDS_TUNING メッセージを v1.8.3 thinking-aware budget の事実に揃える + `docs/lmstudio-direct.md` 新規)
|
|
10
1198
|
|
|
11
1199
|
**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`) として正式化。
|
|
@@ -187,7 +1375,7 @@ M coderouter/doctor.py
|
|
|
187
1375
|
M pyproject.toml
|
|
188
1376
|
M plan.md
|
|
189
1377
|
M docs/troubleshooting.md
|
|
190
|
-
M docs/articles/note-v1-8-1-reality-check.md (or new file v1-8-2)
|
|
1378
|
+
M docs/articles/v1-saga/note-1-v1-8-1-reality-check.md (or new file v1-8-2)
|
|
191
1379
|
M tests/test_doctor.py
|
|
192
1380
|
```
|
|
193
1381
|
|