coderouter-cli 1.10.0__tar.gz → 1.10.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/CHANGELOG.md +135 -0
  2. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/PKG-INFO +1 -1
  3. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/config/schemas.py +25 -0
  4. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/routing/auto_router.py +55 -8
  5. coderouter_cli-1.10.1/examples/providers.raspberrypi.yaml +298 -0
  6. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/pyproject.toml +1 -1
  7. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_auto_router.py +216 -0
  8. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/.gitignore +0 -0
  9. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/LICENSE +0 -0
  10. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/README.en.md +0 -0
  11. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/README.md +0 -0
  12. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/__init__.py +0 -0
  13. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/__main__.py +0 -0
  14. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/adapters/__init__.py +0 -0
  15. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/adapters/anthropic_native.py +0 -0
  16. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/adapters/base.py +0 -0
  17. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/adapters/openai_compat.py +0 -0
  18. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/adapters/registry.py +0 -0
  19. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/cli.py +0 -0
  20. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/cli_stats.py +0 -0
  21. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/config/__init__.py +0 -0
  22. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/config/capability_registry.py +0 -0
  23. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/config/env_file.py +0 -0
  24. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/config/loader.py +0 -0
  25. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/cost.py +0 -0
  26. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/data/__init__.py +0 -0
  27. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/data/model-capabilities.yaml +0 -0
  28. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/doctor.py +0 -0
  29. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/doctor_apply.py +0 -0
  30. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/env_security.py +0 -0
  31. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/errors.py +0 -0
  32. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/guards/__init__.py +0 -0
  33. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/guards/backend_health.py +0 -0
  34. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/guards/memory_pressure.py +0 -0
  35. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/guards/tool_loop.py +0 -0
  36. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/ingress/__init__.py +0 -0
  37. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/ingress/anthropic_routes.py +0 -0
  38. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/ingress/app.py +0 -0
  39. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/ingress/dashboard_routes.py +0 -0
  40. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/ingress/metrics_routes.py +0 -0
  41. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/ingress/openai_routes.py +0 -0
  42. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/logging.py +0 -0
  43. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/metrics/__init__.py +0 -0
  44. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/metrics/collector.py +0 -0
  45. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/metrics/prometheus.py +0 -0
  46. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/output_filters.py +0 -0
  47. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/routing/__init__.py +0 -0
  48. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/routing/adaptive.py +0 -0
  49. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/routing/budget.py +0 -0
  50. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/routing/capability.py +0 -0
  51. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/routing/fallback.py +0 -0
  52. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/translation/__init__.py +0 -0
  53. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/translation/anthropic.py +0 -0
  54. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/translation/convert.py +0 -0
  55. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/translation/tool_repair.py +0 -0
  56. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/assets/dashboard-demo.png +0 -0
  57. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  58. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/designs/v1.6-auto-router-verification.md +0 -0
  59. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/designs/v1.6-auto-router.md +0 -0
  60. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/free-tier-guide.en.md +0 -0
  61. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/free-tier-guide.md +0 -0
  62. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/hf-ollama-models.md +0 -0
  63. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/llamacpp-direct.en.md +0 -0
  64. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/llamacpp-direct.md +0 -0
  65. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/lmstudio-direct.en.md +0 -0
  66. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/lmstudio-direct.md +0 -0
  67. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/openrouter-roster/README.md +0 -0
  68. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/openrouter-roster/latest.json +0 -0
  69. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/quickstart.en.md +0 -0
  70. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/quickstart.md +0 -0
  71. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.4.md +0 -0
  72. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.5-verify.md +0 -0
  73. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.5.md +0 -0
  74. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.6.md +0 -0
  75. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.7.md +0 -0
  76. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v1.0-verify.md +0 -0
  77. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v1.0.md +0 -0
  78. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/security.en.md +0 -0
  79. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/security.md +0 -0
  80. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/troubleshooting.en.md +0 -0
  81. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/troubleshooting.md +0 -0
  82. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/usage-guide.en.md +0 -0
  83. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/usage-guide.md +0 -0
  84. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/when-do-i-need-coderouter.en.md +0 -0
  85. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/when-do-i-need-coderouter.md +0 -0
  86. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/examples/.env.example +0 -0
  87. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/examples/providers.auto-custom.yaml +0 -0
  88. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/examples/providers.auto.yaml +0 -0
  89. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/examples/providers.note-2026.yaml +0 -0
  90. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/examples/providers.nvidia-nim.yaml +0 -0
  91. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/examples/providers.yaml +0 -0
  92. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/scripts/demo_traffic.sh +0 -0
  93. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/scripts/openrouter_roster_diff.py +0 -0
  94. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/scripts/verify_v0_5.sh +0 -0
  95. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/scripts/verify_v1_0.sh +0 -0
  96. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/__init__.py +0 -0
  97. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/conftest.py +0 -0
  98. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_adapter_anthropic.py +0 -0
  99. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_backend_health.py +0 -0
  100. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_budget.py +0 -0
  101. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_capability.py +0 -0
  102. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_capability_degraded_payload.py +0 -0
  103. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_capability_registry.py +0 -0
  104. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_capability_registry_cache_control.py +0 -0
  105. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_claude_code_suitability.py +0 -0
  106. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_cli.py +0 -0
  107. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_cli_stats.py +0 -0
  108. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_config.py +0 -0
  109. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_dashboard_endpoint.py +0 -0
  110. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_doctor.py +0 -0
  111. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_doctor_apply.py +0 -0
  112. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_doctor_cache_probe.py +0 -0
  113. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_env_file.py +0 -0
  114. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_env_security.py +0 -0
  115. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_errors.py +0 -0
  116. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_examples_yaml.py +0 -0
  117. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback.py +0 -0
  118. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback_anthropic.py +0 -0
  119. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback_cache_control.py +0 -0
  120. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback_cache_observed.py +0 -0
  121. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback_misconfig_warn.py +0 -0
  122. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback_paid_gate.py +0 -0
  123. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback_thinking.py +0 -0
  124. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_guards_tool_loop.py +0 -0
  125. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_ingress_anthropic.py +0 -0
  126. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_ingress_profile.py +0 -0
  127. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_memory_pressure.py +0 -0
  128. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_cache.py +0 -0
  129. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_collector.py +0 -0
  130. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_cost.py +0 -0
  131. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_endpoint.py +0 -0
  132. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_jsonl.py +0 -0
  133. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_prometheus.py +0 -0
  134. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_prometheus_cache.py +0 -0
  135. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_openai_compat.py +0 -0
  136. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_openrouter_roster_diff.py +0 -0
  137. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_output_filters.py +0 -0
  138. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_output_filters_adapters.py +0 -0
  139. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_reasoning_strip.py +0 -0
  140. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_routing_adaptive.py +0 -0
  141. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_setup_sh.py +0 -0
  142. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_tool_repair.py +0 -0
  143. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_translation_anthropic.py +0 -0
  144. {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_translation_reverse.py +0 -0
@@ -6,6 +6,141 @@ versioning follows [SemVer](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [v1.10.1] — 2026-05-04 (Patch — tool-aware auto routing + Raspberry Pi starter)
10
+
11
+ **Theme: 「ローカル小型モデルでは tool calling できないので tool-laden な request だけクラウドに逃がしたい」というユースケース (OpenClaw + Pi 8GB シナリオ) を declarative に解決。** v1.10.0 で feature complete を宣言した auto_router の 6 matcher を 7 matcher に拡張、`has_tools` を追加して「tools[] を宣言したリクエストか否か」で profile を分岐できるように。併せて Raspberry Pi 8GB 向けの starter YAML (`examples/providers.raspberrypi.yaml`) を同梱、SBC 上で OpenClaw / Claude Code 互換 agent を回すユーザーが yaml 1 個 copy するだけで動く状態にした。
12
+
13
+ 含まれる出荷 2 件:
14
+
15
+ | # | sub-release | テーマ | LOC | tests |
16
+ |---|---|---|---|---|
17
+ | 1 | **has_tools matcher** | `RuleMatcher.has_tools` 7 番目 matcher 追加、OpenAI/Anthropic `tools[]` + OpenAI legacy `functions[]` を一括認識 (OpenClaw + Pi 由来) | ~80 | +7 |
18
+ | 2 | **Raspberry Pi starter** | `examples/providers.raspberrypi.yaml` 新規、Ollama 小型モデル (≤4B) + OpenRouter free 系 + `has_tools` ベースの tool-aware profile 振り分け | YAML のみ | (loader 検証で +0 直接、既存 parametric test に乗る) |
19
+
20
+ - Tests: 871 → **878** (+7、has_tools matcher の 6 シナリオ + `has_tools: false` の "set 扱いだがマッチしない" 安全網テスト)
21
+ - Runtime deps: 5 → 5 (**34 sub-release 連続据え置き**)
22
+ - Backward compat: 完全互換、既存 yaml / API / log payload schema 完全に同じ、新フィールド (`has_tools`) を使わない deployment は挙動完全一致
23
+ - pyproject version: 1.10.0 → 1.10.1
24
+
25
+ ### Migration
26
+
27
+ 不要。**v1.10.0 からの自然なアップグレード**:
28
+
29
+ - `coderouter` コマンド名 / Python import 名 / providers.yaml の format / env 変数 / ingress URL すべて完全に同じ
30
+ - 既存 `auto_router.rules[]` は何も変わらない、`has_tools` matcher を使い始めるには yaml に 1 行足すだけ
31
+ - v1.10.0 で v1.6 系 auto_router を「6 matcher で feature complete」と宣言した直後の追加だが、同じ宣言型 framework の延長線で構造変更なし — 「7 matcher で改めて feature complete」と読み替えて差し支えない
32
+
33
+ ### Out of scope (v1.11 以降)
34
+
35
+ - **Provider capability gate for tools** — `capabilities.tools=false` を fallback chain の skip ゲートとして機能させる案。本 patch は profile レベルで振り分ける方針 (router で chain を切り替える) で `has_tools` matcher を採用、provider レベルの skip ゲートは別 issue。CodeRouter の chain semantics (順次フォールバック + downgrade) の互換性検討が必要なため、必要性が確認できてから着手。
36
+ - **小型ローカルモデルの tool-call repair 強化** — 現状 `tool_repair.py` は `<tool_call>{...}</tool_call>` ラッパ形式の救済を行うが、1-4B モデルが返す自由形式の text からの推測救済は別領域 (`tool_emulation`)。プロンプトテンプレ書き換えで誘導する手段もあり、設計検討は v2.0 後送り。
37
+
38
+ ### Files touched
39
+
40
+ ```
41
+ A examples/providers.raspberrypi.yaml
42
+ M CHANGELOG.md
43
+ M coderouter/config/schemas.py
44
+ M coderouter/routing/auto_router.py
45
+ M pyproject.toml
46
+ M tests/test_auto_router.py
47
+ ```
48
+
49
+ ---
50
+
51
+ ### has_tools matcher (OpenClaw + Raspberry Pi 由来)
52
+
53
+ **Theme: tools[] を宣言したリクエストだけクラウドに振り分け、ローカル小型モデルは tool 不要の素朴な chat に専念させる。** Raspberry Pi 8GB / Jetson Nano クラスの SBC で OpenClaw 等の tool-aware agent を動かしたい時、CPU 推論で実用域に入る Ollama モデル (≤4B) は tool calling が苦手 (`finish_reason: tool_calls` を返さない / 引数 JSON が壊れる / 自由形式 text に bury される) で、結果として agent 側からは「tool 呼び出しが起きてない」状態になる。`auto_router.rules[].if.has_tools` を 7 番目の matcher として追加することで、profile レベルで「tools あり → クラウド (Qwen3-Coder/gpt-oss/Gemini-Flash の OpenRouter free)」「tools 無し → ローカル小型」を declarative に切り替えられる。
54
+
55
+ ユースケース例 (Raspberry Pi 8GB starter `examples/providers.raspberrypi.yaml` から抜粋):
56
+
57
+ ```yaml
58
+ auto_router:
59
+ rules:
60
+ - id: user:has-tools-go-cloud
61
+ profile: with-tools # OpenRouter free 系のみ
62
+ match:
63
+ has_tools: true
64
+ - id: user:image-go-cloud
65
+ profile: vision # Gemini Flash 1M ctx
66
+ match:
67
+ has_image: true
68
+ - id: user:longcontext-go-cloud
69
+ profile: longcontext
70
+ match:
71
+ content_token_count_min: 32000
72
+ default_rule_profile: local-chat # qwen3.5:2b/4b / gemma3:1b ローカル
73
+ ```
74
+
75
+ OpenClaw (毎ターン Bash/Read/Write 等の tool を declare する agent) を `OPENAI_BASE_URL=http://<pi-ip>:8088/v1` で繋ぐと、tool-laden traffic は自動でクラウドに飛び、軽い chat だけが Pi 上のローカルで処理される。OPENROUTER_API_KEY のみ設定すればよく、有料 API は不要 (`ALLOW_PAID=false` がデフォルト)。
76
+
77
+ #### なぜ provider レベルの capability gate ではなく profile レベルなのか
78
+
79
+ `ProviderConfig.capabilities.tools=false` フラグは既存 (v0.x からある) だが、現状は `coderouter doctor` の診断表示と `model-capabilities.yaml` registry の解決に使うだけで、fallback chain における skip ゲートには接続されていない。`thinking` / `cache_control` には `will_degrade` ゲート (capability.py の `provider_supports_*`) があるが、tools には同等の skip 機構がない。これは既存の v0.3-D 「downgrade path」(non-native + tools[] あり → 非ストリーミング + tool_repair) に依存していて、provider が tools を返せなくても adapter エラーは出ず、上流から見ると success (空 tool_calls) で chain が fallthrough せず止まってしまう (= 観測症状: 「tool call されてない」)。
80
+
81
+ provider レベルの skip ゲートを後付けするのは chain semantics に踏み込む変更で互換性検討が要るため、本 patch では **profile レベルの宣言型 lever** に留める方針を採用。chain semantics を変えず、auto_router rule の追加で同じ効果を得られ、かつ既存の 6 matcher と完全に同じ規約 (exactly one + first match wins + fast-fail at load) で導入できる。
82
+
83
+ - Tests: 871 → **878** (+7: OpenAI tools[] / Anthropic tools[] / OpenAI legacy functions[] / no-tools fallthrough / 空リスト fallthrough / has_tools rule が code-fence rule より優先 / `has_tools: false` の "set 扱いだがマッチしない" 安全網)
84
+ - Runtime deps: 5 → 5 (34 sub-release 連続据え置き)
85
+ - Backward compat: 完全互換、既存 `auto_router` rule は何も変わらない、`has_tools` を使わない deployment は挙動完全一致
86
+
87
+ #### Changes
88
+
89
+ - `coderouter/config/schemas.py`:
90
+ - `RuleMatcher` に `has_tools: bool | None = None` を追加、`_MATCHER_FIELDS` tuple に追加 (zero/multiple-fields の "exactly one" バリデータが自動適用)。
91
+ - docstring の Variants セクションに 7 番目として `has_tools` を追記、boolean 形状が `has_image` と同じである理由 (`True` のみ意味を持ち、`False` は "set" 扱いだが `_match_rule` の `is True` チェックでマッチしない安全網) と、provider レベルの `capabilities.tools` flag との違い (前者は profile-level routing、後者は doctor の診断補助で chain skip ゲートではない) を明示。
92
+
93
+ - `coderouter/routing/auto_router.py`:
94
+ - `_has_tools_in_body(body)` ヘルパを新設 — body の top-level `tools[]` (OpenAI Chat Completions / Anthropic Messages API 共通) と `functions[]` (OpenAI legacy、deprecated だが pinned SDK で残存) を一括認識、空リスト / None は False (lazy init 対応)。
95
+ - `_match_rule(rule, message, text, model, estimated_tokens, has_tools)` シグネチャに `has_tools: bool` を追加、`has_tools is True` 分岐を 7 番目として実装。
96
+ - `classify(...)` 内で `_has_tools_in_body(body)` を一度だけ呼んで rule iteration に渡す。`user_msg is None` でも `has_tools` rule は評価する (system-only prompt + tools[] declaration の構成にも対応)。
97
+ - `_emit_resolved` / `_emit_fallthrough` の `signals` payload に `has_tools` を追記、`auto-router-resolved` log で「tools あり判定で routing したか」が dashboard / Prometheus exporter から見える。
98
+
99
+ - `tests/test_auto_router.py` Group 8 (tool-aware routing) を新設、7 ケース:
100
+ - `test_classify_request_with_openai_tools_routes_to_with_tools` — 基本ケース、OpenAI 形式 `tools[].function` → `with-tools` profile。
101
+ - `test_classify_request_with_anthropic_tools_routes_to_with_tools` — Anthropic 形式 `tools[].input_schema` も同じ top-level `tools` キーなので、単一 matcher で両 ingress カバー。
102
+ - `test_classify_request_with_legacy_functions_routes_to_with_tools` — OpenAI legacy `functions[]` (deprecated だが pinned SDK で残存) も tool-laden 扱い。
103
+ - `test_classify_request_without_tools_falls_through` — 逆ケース、tools 宣言なしの plain chat は `default_rule_profile` (Pi の場合は `local-chat`) に落ちる。
104
+ - `test_classify_empty_tools_list_treated_as_no_tools` — `tools: []` / `functions: []` (lazy init shape) は False 扱い、no-spurious-match property を pin。
105
+ - `test_classify_has_tools_first_match_wins_over_later_content_rule` — has_tools rule が code_fence rule より前に置かれた時、両方マッチする body でも先勝、global "first match wins" を新 matcher にも適用。
106
+ - `test_has_tools_false_rejected_at_load` — `has_tools: False` が `_exactly_one` を通過するが `_match_rule` の `is True` チェックでマッチしない安全網を文書化、誤設定時もデフォルト経路に落ちることを保証。
107
+
108
+ #### Files touched
109
+
110
+ ```
111
+ M CHANGELOG.md
112
+ M coderouter/config/schemas.py
113
+ M coderouter/routing/auto_router.py
114
+ M pyproject.toml
115
+ M tests/test_auto_router.py
116
+ ```
117
+
118
+ ---
119
+
120
+ ### Raspberry Pi 8GB starter (`examples/providers.raspberrypi.yaml`)
121
+
122
+ **Theme: SBC で OpenClaw を動かす最小構成を yaml 1 個に集約。** v1.10.1 で追加した `has_tools` matcher を主役にした starter で、`coderouter serve` 1 発で Pi 上のローカル ollama (qwen3.5:2b/4b、qwen2.5:1.5b、gemma3:1b) と OpenRouter free 系 (qwen3-coder:free / gpt-oss-120b:free / gemini-2.5-flash:free) が tool-aware に振り分けられる。OPENROUTER_API_KEY のみ要設定、有料 API キー不要 (`ALLOW_PAID=false` がデフォルト)。
123
+
124
+ #### 設計の要点
125
+
126
+ - **ローカル全部 `tools: false`** — Pi 8GB に乗る ≤4B モデルは tool_calls を安定して返せないため capability で明示的に `false`。これは doctor 診断用の宣言で、実 routing は `has_tools` matcher が profile レベルで振り分けるので二重防御になる。
127
+ - **`num_ctx: 8192` + `num_predict: 1024` 制限** — Pi の CPU 推論は context 縮めた方が prefill が現実的、デフォルト ollama の 2048 だと OpenClaw の system prompt で詰む & 2048 から 32K に上げると prefill が分単位になるので 8K が現実的中間点。
128
+ - **画像 / 長尺 (32K+) もクラウドへ** — Pi では Gemma 4 E4B (vision capable だが 9.6GB で 8GB Pi に乗らない) の代わりに、`has_image` rule で OpenRouter Gemini Flash (1M ctx + vision native) に逃がす。
129
+ - **OpenRouter free 3 モデルで vendor diversity** — qwen-coder / gpt-oss / gemini-flash の 3 ベンダーを並べて、daily cap (~200 req/day per model per account) 当たり時の rate-limit 逃げ場を確保。
130
+ - **`output_filters: [strip_thinking, strip_stop_markers]` を Qwen 系で常時適用** — Pi で動かす Qwen 3.5 系は `<think>...</think>` リーク + `<|im_end|>` 漏れの両方を観測、両方 strip。
131
+
132
+ #### Tests
133
+
134
+ `tests/test_examples_yaml.py::test_example_yaml_loads` が `examples/providers*.yaml` を parametric にカバーしているため、`providers.raspberrypi.yaml` も自動でこの test に乗る。新たに pin したい invariant (例: ローカル全部 `tools: false`、`has_tools` rule の存在、auto_router default が `local-chat` 等) があれば後続 patch で個別 test 追加可能だが、本 patch では parametric の loader-clean property のみ確保。
135
+
136
+ #### Files touched
137
+
138
+ ```
139
+ A examples/providers.raspberrypi.yaml
140
+ ```
141
+
142
+ ---
143
+
9
144
  ## [v1.10.0] — 2026-05-01 (Umbrella tag — Cost enforcement + Long-run reliability completion + Auto-router feature complete)
10
145
 
11
146
  **Theme: 「観測 → 理解 → 行動」を 3 軸で完成、Vision pillar P2/P3 が揃う。** v1.9.1 (patch) で取り切った 2 機能 (v1.9-B2 streaming usage 集約 + per-model auto-routing) は事実上 v1.10 backlog の助走、本 v1.10.0 で残り 3 機能を minor として束ねて出荷。CodeRouter は **「Local LLM で agent を長時間回すための信頼性層」** という Vision の v1.x 担当分が完成 — context overflow (L1) と quality drift (L4) を除く 4 系統障害 (L2/L3/L5/L6) を体系的に対処、auto-router の declarative 6 matcher も揃い、cost 系は観測 (v1.9-D) → enforcement (v1.10) で経路が閉じた。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 1.10.0
3
+ Version: 1.10.1
4
4
  Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
5
5
  Project-URL: Homepage, https://github.com/zephel01/CodeRouter
6
6
  Project-URL: Repository, https://github.com/zephel01/CodeRouter
@@ -498,6 +498,23 @@ class RuleMatcher(BaseModel):
498
498
  workloads can compensate by tuning the threshold, since the
499
499
  char/4 heuristic is conservative for CJK and looser for
500
500
  English code.
501
+
502
+ Variants ([Unreleased] / tool-aware routing, OpenClaw + Pi 由来):
503
+
504
+ - ``has_tools: True`` — the request body declares one or more
505
+ tools (OpenAI ``tools[]`` / Anthropic ``tools[]`` / OpenAI legacy
506
+ ``functions[]``). Lets operators send tool-laden requests to a
507
+ tool-capable cloud profile while keeping plain chat on a small
508
+ local model (typical Raspberry Pi / low-spec deployment shape:
509
+ a 1-4B local model that cannot reliably tool-call paired with a
510
+ free-tier cloud chain that can). Distinct from the
511
+ ``capabilities.tools`` flag on a provider — that flag is read by
512
+ ``coderouter doctor`` for diagnostics but does NOT gate the
513
+ fallback chain (the chain just iterates providers in order and
514
+ engages the v0.3-D tool-downgrade path on non-native ones with
515
+ ``request.tools`` set). The ``has_tools`` matcher is the
516
+ profile-level lever for steering tool-laden traffic to the right
517
+ chain entirely.
501
518
  """
502
519
 
503
520
  model_config = ConfigDict(extra="forbid")
@@ -508,6 +525,13 @@ class RuleMatcher(BaseModel):
508
525
  content_regex: str | None = None
509
526
  model_pattern: str | None = None
510
527
  content_token_count_min: int | None = Field(default=None, ge=1)
528
+ # [Unreleased]: tool-aware routing (OpenClaw + Raspberry Pi 由来).
529
+ # See class docstring "Variants ([Unreleased] / tool-aware routing)"
530
+ # above for the full rationale. Boolean shape mirrors ``has_image`` —
531
+ # only the ``True`` value is meaningful (matches when the body
532
+ # declares any tools); ``False`` is rejected by ``_exactly_one``
533
+ # since a "no-tools" rule would shadow the default fall-through.
534
+ has_tools: bool | None = None
511
535
 
512
536
  _MATCHER_FIELDS: tuple[str, ...] = (
513
537
  "has_image",
@@ -516,6 +540,7 @@ class RuleMatcher(BaseModel):
516
540
  "content_regex",
517
541
  "model_pattern",
518
542
  "content_token_count_min",
543
+ "has_tools",
519
544
  )
520
545
 
521
546
  @model_validator(mode="after")
@@ -142,12 +142,40 @@ def _code_fence_ratio(text: str) -> float:
142
142
  return fenced / len(text)
143
143
 
144
144
 
145
+ def _has_tools_in_body(body: dict[str, Any]) -> bool:
146
+ """True iff the request body declares one or more callable tools.
147
+
148
+ Recognized declaration shapes:
149
+
150
+ * ``tools: [...]`` — OpenAI Chat Completions ``tools[]`` AND
151
+ Anthropic Messages API ``tools[]``. Both wire formats put the
152
+ array at the same top-level key, so a single membership check
153
+ covers both ingresses.
154
+ * ``functions: [...]`` — OpenAI legacy ``functions[]`` (deprecated
155
+ since 2023-11 but still emitted by some agents that pinned old
156
+ SDK versions). Treated as equivalent to ``tools[]`` for routing
157
+ purposes.
158
+
159
+ A non-list value (or a value of ``None`` / empty list) returns
160
+ False — agents that initialize the field but populate it lazily
161
+ are still on the no-tools path until a tool actually appears.
162
+ """
163
+ tools = body.get("tools")
164
+ if isinstance(tools, list) and len(tools) > 0:
165
+ return True
166
+ functions = body.get("functions")
167
+ if isinstance(functions, list) and len(functions) > 0:
168
+ return True
169
+ return False
170
+
171
+
145
172
  def _match_rule(
146
173
  rule: AutoRouteRule,
147
174
  message: dict[str, Any] | None,
148
175
  text: str,
149
176
  model: str | None,
150
177
  estimated_tokens: int,
178
+ has_tools: bool,
151
179
  ) -> bool:
152
180
  m = rule.match
153
181
  if m.has_image is True:
@@ -177,6 +205,14 @@ def _match_rule(
177
205
  # for English code; operators tune the threshold to match
178
206
  # their input distribution.
179
207
  return estimated_tokens >= m.content_token_count_min
208
+ if m.has_tools is True:
209
+ # [Unreleased]: tool-aware routing (OpenClaw + Pi 由来).
210
+ # Computed once in ``classify`` from ``body.tools`` /
211
+ # ``body.functions`` so per-rule evaluation is O(1). See
212
+ # ``_has_tools_in_body`` for the recognized declaration shapes
213
+ # and ``RuleMatcher`` docstring for why this is profile-level
214
+ # routing (not a provider capability gate).
215
+ return has_tools
180
216
  return False # pragma: no cover — _exactly_one guards against this
181
217
 
182
218
 
@@ -259,6 +295,11 @@ def classify(body: dict[str, Any], config: CodeRouterConfig) -> str:
259
295
  # contribute 0. See ``_estimate_total_tokens`` for the heuristic
260
296
  # rationale and the 5-deps tradeoff.
261
297
  estimated_tokens = _estimate_total_tokens(body)
298
+ # [Unreleased]: tool-aware routing (OpenClaw + Pi 由来). Computed
299
+ # once for both the ``has_tools`` matcher and the signals payload.
300
+ # See ``_has_tools_in_body`` for the recognized declaration shapes
301
+ # (OpenAI/Anthropic ``tools[]``, OpenAI legacy ``functions[]``).
302
+ has_tools = _has_tools_in_body(body)
262
303
 
263
304
  auto_cfg = config.auto_router
264
305
  if auto_cfg is not None and auto_cfg.disabled:
@@ -267,6 +308,7 @@ def classify(body: dict[str, Any], config: CodeRouterConfig) -> str:
267
308
  text,
268
309
  model,
269
310
  estimated_tokens,
311
+ has_tools,
270
312
  disabled=True,
271
313
  )
272
314
  return auto_cfg.default_rule_profile
@@ -278,17 +320,18 @@ def classify(body: dict[str, Any], config: CodeRouterConfig) -> str:
278
320
  else BUNDLED_DEFAULT_RULE_PROFILE
279
321
  )
280
322
 
281
- # ``model_pattern`` and ``content_token_count_min`` matchers can
282
- # fire even without a user message (e.g. system-only prompts or a
283
- # request body that carries only a model field). Other matchers
284
- # still require ``user_msg`` to be present — they short out via
285
- # ``_match_rule``'s message-None handling.
323
+ # ``model_pattern``, ``content_token_count_min`` and ``has_tools``
324
+ # matchers can fire even without a user message (e.g. system-only
325
+ # prompts or a request body that carries only a model field +
326
+ # tools array). Other matchers still require ``user_msg`` to be
327
+ # present — they short out via ``_match_rule``'s message-None
328
+ # handling.
286
329
  for rule in rules:
287
- if _match_rule(rule, user_msg, text, model, estimated_tokens):
288
- _emit_resolved(rule, user_msg, text, model, estimated_tokens)
330
+ if _match_rule(rule, user_msg, text, model, estimated_tokens, has_tools):
331
+ _emit_resolved(rule, user_msg, text, model, estimated_tokens, has_tools)
289
332
  return rule.profile
290
333
 
291
- _emit_fallthrough(default_profile, text, model, estimated_tokens)
334
+ _emit_fallthrough(default_profile, text, model, estimated_tokens, has_tools)
292
335
  return default_profile
293
336
 
294
337
 
@@ -298,6 +341,7 @@ def _emit_resolved(
298
341
  text: str,
299
342
  model: str | None,
300
343
  estimated_tokens: int,
344
+ has_tools: bool,
301
345
  ) -> None:
302
346
  logger.info(
303
347
  "auto-router-resolved",
@@ -310,6 +354,7 @@ def _emit_resolved(
310
354
  "content_len": len(text),
311
355
  "model": model,
312
356
  "estimated_tokens": estimated_tokens,
357
+ "has_tools": has_tools,
313
358
  },
314
359
  },
315
360
  )
@@ -320,6 +365,7 @@ def _emit_fallthrough(
320
365
  text: str,
321
366
  model: str | None,
322
367
  estimated_tokens: int,
368
+ has_tools: bool,
323
369
  disabled: bool = False,
324
370
  ) -> None:
325
371
  logger.info(
@@ -331,6 +377,7 @@ def _emit_fallthrough(
331
377
  "content_len": len(text),
332
378
  "model": model,
333
379
  "estimated_tokens": estimated_tokens,
380
+ "has_tools": has_tools,
334
381
  "disabled": disabled,
335
382
  },
336
383
  },
@@ -0,0 +1,298 @@
1
+ # ============================================================
2
+ # CodeRouter providers.yaml — Raspberry Pi 8GB starter
3
+ #
4
+ # Use case (OpenClaw + Pi 由来):
5
+ # 小型 SBC (Raspberry Pi 4/5 8GB、Jetson Nano 等) で OpenClaw 等の
6
+ # tool-aware エージェントを動かしたい。CPU 推論オンリーで実用域に
7
+ # 入る Ollama モデル (≤4B) はそもそも tool calling が苦手 — 7B 以上
8
+ # 無いと native tool_calls を安定して返さない。
9
+ #
10
+ # この starter は「**ローカルは tool 無し前提**、tool 必要なリクエスト
11
+ # は無料クラウドにフォワード」という割り切りで構成される:
12
+ #
13
+ # - 軽い chat / 要約 / 翻訳 / 分類 → ローカル小型モデル
14
+ # - tools[] / functions[] が宣言された → OpenRouter free tier
15
+ # - 画像入力 → OpenRouter (Gemini Flash 1M ctx)
16
+ #
17
+ # v1.6 auto_router の ``has_tools`` matcher (本リリース新規) で
18
+ # profile レベルで振り分けるため、ユーザーは ``coderouter serve`` を
19
+ # 起動するだけ — クライアント側 (OpenClaw / Claude Code) で profile
20
+ # を意識する必要は無い。
21
+ #
22
+ # Hardware budget (Raspberry Pi 4/5 8GB):
23
+ # - 実メモリは OS 込みで ~6GB 使える
24
+ # - GPU 無し、CPU 推論のみ (3-15 tok/s 程度)
25
+ # - tool calling が安定するのは 7B+ だがメモリ不足で動かない
26
+ # - 妥協ライン: 1.5B〜4B のローカル + tool は全部クラウド
27
+ #
28
+ # Setup (3 step / 約 5-10 分):
29
+ #
30
+ # # 1. Ollama on Pi (公式 install script)
31
+ # curl -fsSL https://ollama.com/install.sh | sh
32
+ #
33
+ # # 2. このリポジトリの想定モデルを pull (合計 ~6GB)
34
+ # ollama pull qwen2.5:1.5b # 軽量 chat (~1GB)
35
+ # ollama pull gemma3:1b # 多言語 chat (~0.8GB)
36
+ # ollama pull qwen3.5:2b # よりまともな chat (~2.7GB)
37
+ # ollama pull qwen3.5:4b # しっかり chat (~3.4GB) — 余裕があれば
38
+ # ollama pull sam860/lfm2.5:350m # 超軽量・常駐用 (~0.5GB) — 任意
39
+ #
40
+ # # 3. OpenRouter API key を ~/.coderouter/.env に書く
41
+ # # https://openrouter.ai/ で無料アカウント作って API key 取得 (5min)
42
+ # echo 'OPENROUTER_API_KEY=sk-or-v1-...' >> ~/.coderouter/.env
43
+ #
44
+ # # 4. このファイルを copy + 起動
45
+ # cp examples/providers.raspberrypi.yaml ~/.coderouter/providers.yaml
46
+ # coderouter serve
47
+ #
48
+ # Free-tier rate limits (OpenRouter, 2026-04 確認):
49
+ # ~20 req/min, ~200 req/day per model per account.
50
+ # tools-heavy エージェント (毎ターン tool 宣言する OpenClaw / Claude Code)
51
+ # だと daily cap に当たりやすい。3 つの free モデル (qwen / gpt-oss /
52
+ # gemini-flash) を rate-limit 逃げ用に並べてあるので、1 つ 429 を
53
+ # 返したら次に fallback する設計。
54
+ #
55
+ # OpenClaw 連携:
56
+ # OpenClaw は OpenAI 互換 API で叩ける (env で base_url 上書き):
57
+ # export OPENAI_BASE_URL=http://<pi-ip>:8088/v1
58
+ # export OPENAI_API_KEY=dummy # CodeRouter は鍵を見ない
59
+ # これで OpenClaw のリクエストは全部 CodeRouter を経由し、
60
+ # ``has_tools`` で振り分けられる。
61
+ # ============================================================
62
+
63
+ allow_paid: false
64
+ default_profile: auto # ← v1.6 sentinel: auto_router を有効化
65
+ display_timezone: Asia/Tokyo
66
+
67
+ # ---------------------------------------------------------------------
68
+ # auto_router: tool-aware ルーティング (v[Unreleased] has_tools matcher)
69
+ #
70
+ # 4 ルールで Pi のユースケースを尽くす:
71
+ #
72
+ # 1. tools[] あり → ``with-tools`` (クラウド free 専用)
73
+ # 2. 画像あり → ``vision`` (Gemini Flash 1M ctx)
74
+ # 3. プロンプト超長尺 → ``longcontext``(Gemini Flash 1M ctx)
75
+ # 4. それ以外 → ``local-chat`` (ローカル小型モデル)
76
+ #
77
+ # 順序は「絶対にローカルに流せないもの (tools / image / 超長尺) を上に
78
+ # 寄せて、最後にローカル fallthrough」が原則。
79
+ # ---------------------------------------------------------------------
80
+
81
+ auto_router:
82
+ rules:
83
+ # 1) tools / functions 宣言があるリクエストは即クラウド。
84
+ # Pi 上の 1-4B ローカルモデルでは tool_calls を安定して返せない
85
+ # (text に bury される / format が崩れる / そもそも生成しない)
86
+ # ので、router 段階で local を skip するのが一番確実。
87
+ - id: user:has-tools-go-cloud
88
+ profile: with-tools
89
+ match:
90
+ has_tools: true
91
+
92
+ # 2) 画像入力はローカル小型モデルが扱えない (Gemma 4 E4B は vision
93
+ # 対応だが Pi 8GB に乗らない 9.6GB)。1M ctx の Gemini Flash 無料枠で
94
+ # 捌くのが妥当。
95
+ - id: user:image-go-cloud
96
+ profile: vision
97
+ match:
98
+ has_image: true
99
+
100
+ # 3) 超長尺リクエスト (~32K token 超) — Pi 上の小型モデルは context
101
+ # が 8K-32K 程度しか無いことが多く、prefill も遅い。1M ctx の
102
+ # Gemini Flash に逃がす。
103
+ - id: user:longcontext-go-cloud
104
+ profile: longcontext
105
+ match:
106
+ content_token_count_min: 32000
107
+
108
+ # 上記いずれにも該当しない (= 軽い chat) はローカル先頭の chain で。
109
+ default_rule_profile: local-chat
110
+
111
+ # ---------------------------------------------------------------------
112
+ # providers
113
+ # ---------------------------------------------------------------------
114
+
115
+ providers:
116
+ # ============================================================
117
+ # Tier 1: ローカル (Raspberry Pi 8GB on-host Ollama)
118
+ # ============================================================
119
+ # 全部 ``tools: false`` 宣言。fallback 時の「tools 必要 → cloud」は
120
+ # 上の auto_router で振り分け済みなので、このローカル群が tool-laden
121
+ # なリクエストを受けることは無い (前提).
122
+
123
+ # qwen3.5:4b — このラインナップでは最大、しっかり chat 用途向け。
124
+ # 推論速度は Pi 4 で ~3 tok/s、Pi 5 で ~6 tok/s 程度。
125
+ # メモリは Q4_K_M 量子化で ~3.4GB、Pi 8GB に余裕で乗る。
126
+ - name: ollama-qwen3-5-4b
127
+ kind: openai_compat
128
+ base_url: http://localhost:11434/v1
129
+ model: qwen3.5:4b
130
+ paid: false
131
+ timeout_s: 240 # CPU 推論なので generous に
132
+ extra_body:
133
+ options:
134
+ # Pi の CPU 推論は context 縮めた方が prefill が現実的。
135
+ # 8K で OpenClaw の system prompt + 軽い turn を捌く想定。
136
+ num_ctx: 8192
137
+ num_predict: 1024
138
+ # Qwen 系は <think> リーク、stop marker 漏れがあるので scrub 推奨。
139
+ output_filters: [strip_thinking, strip_stop_markers]
140
+ capabilities:
141
+ chat: true
142
+ streaming: true
143
+ tools: false # 4B 帯では tool_calls が安定しない
144
+
145
+ # qwen3.5:2b — 4b より速い。短い応答が欲しいときの primary。
146
+ # Pi 5 で ~10-12 tok/s、~2.7GB メモリ。
147
+ - name: ollama-qwen3-5-2b
148
+ kind: openai_compat
149
+ base_url: http://localhost:11434/v1
150
+ model: qwen3.5:2b
151
+ paid: false
152
+ timeout_s: 180
153
+ extra_body:
154
+ options:
155
+ num_ctx: 8192
156
+ num_predict: 1024
157
+ output_filters: [strip_thinking, strip_stop_markers]
158
+ capabilities:
159
+ chat: true
160
+ streaming: true
161
+ tools: false # 2B 帯では tool_calls が安定しない
162
+
163
+ # qwen2.5:1.5b — 軽量・速い、簡単な応答用。
164
+ # Pi 5 で ~15 tok/s、~1GB メモリ。
165
+ - name: ollama-qwen2-5-1-5b
166
+ kind: openai_compat
167
+ base_url: http://localhost:11434/v1
168
+ model: qwen2.5:1.5b
169
+ paid: false
170
+ timeout_s: 120
171
+ extra_body:
172
+ options:
173
+ num_ctx: 8192
174
+ num_predict: 512
175
+ capabilities:
176
+ chat: true
177
+ streaming: true
178
+ tools: false # 1.5B では tool_calls 不能
179
+
180
+ # gemma3:1b — Google Gemma 3 系の小型モデル、多言語性能良好。
181
+ # qwen2.5:1.5b の代替として CJK / 欧州言語の応答質が安定する場面あり。
182
+ - name: ollama-gemma3-1b
183
+ kind: openai_compat
184
+ base_url: http://localhost:11434/v1
185
+ model: gemma3:1b
186
+ paid: false
187
+ timeout_s: 120
188
+ extra_body:
189
+ options:
190
+ num_ctx: 8192
191
+ num_predict: 512
192
+ capabilities:
193
+ chat: true
194
+ streaming: true
195
+ tools: false # 1B では tool_calls 不能
196
+
197
+ # ============================================================
198
+ # Tier 2: 無料クラウド (OpenRouter free tier、tool-capable)
199
+ # ============================================================
200
+ # 全部 free 階層。レート制限は ~20 req/min / ~200 req/day 程度
201
+ # (per model, per account)。3 つの異なるベンダーを並べて rate-limit
202
+ # 当たった時の逃げ場を確保。
203
+
204
+ # OpenRouter の Qwen3-Coder 480B free — agentic coding 専用設計、
205
+ # tool calling が極めて安定 (CodeRouter で v0.3-A repair 不要)。
206
+ - name: openrouter-qwen-coder-free
207
+ kind: openai_compat
208
+ base_url: https://openrouter.ai/api/v1
209
+ model: qwen/qwen3-coder:free
210
+ api_key_env: OPENROUTER_API_KEY
211
+ paid: false
212
+ timeout_s: 60
213
+ capabilities:
214
+ chat: true
215
+ streaming: true
216
+ tools: true
217
+
218
+ # OpenRouter の OpenAI gpt-oss-120b free — tool calling 対応、
219
+ # Qwen-Coder と異なるベンダー pool なので daily cap 当たり時の逃げ場。
220
+ - name: openrouter-gpt-oss-free
221
+ kind: openai_compat
222
+ base_url: https://openrouter.ai/api/v1
223
+ model: openai/gpt-oss-120b:free
224
+ api_key_env: OPENROUTER_API_KEY
225
+ paid: false
226
+ timeout_s: 60
227
+ capabilities:
228
+ chat: true
229
+ streaming: true
230
+ tools: true
231
+
232
+ # OpenRouter の Gemini 2.5 Flash free — 1M ctx + vision、超長尺と
233
+ # 画像入力で primary。tool calling もサポート。
234
+ - name: openrouter-gemini-flash-free
235
+ kind: openai_compat
236
+ base_url: https://openrouter.ai/api/v1
237
+ model: google/gemini-2.5-flash:free
238
+ api_key_env: OPENROUTER_API_KEY
239
+ paid: false
240
+ timeout_s: 60
241
+ capabilities:
242
+ chat: true
243
+ streaming: true
244
+ vision: true
245
+ tools: true
246
+
247
+ # ---------------------------------------------------------------------
248
+ # profiles — auto_router rules が指す 4 つの profile を定義
249
+ # ---------------------------------------------------------------------
250
+
251
+ profiles:
252
+ # ローカル先頭、軽量から段階的に大きく。クラウドは最終保険のみ。
253
+ # 「Pi で完結させたい軽い chat」用 — tools 必要なら router が cloud に
254
+ # 振り分けるので、ここに来る時点で tools 不要が確定している。
255
+ - name: local-chat
256
+ append_system_prompt: |
257
+ Be concise and direct. Aim for 1-3 sentence answers unless the
258
+ question genuinely needs more. You are running on a small local
259
+ model — do not pretend to have capabilities you don't have.
260
+ providers:
261
+ - ollama-qwen3-5-2b # primary: 速度と質のバランス
262
+ - ollama-qwen2-5-1-5b # 速度優先 fallback
263
+ - ollama-gemma3-1b # 多言語 fallback
264
+ - ollama-qwen3-5-4b # 質優先 fallback (遅め)
265
+ - openrouter-gpt-oss-free # ローカル全滅時の最終保険
266
+
267
+ # tool-laden リクエスト専用 — クラウド先頭、tool_calls 安定優先。
268
+ # local モデルは一切含めない (tools 不能なので意味が無い)。
269
+ - name: with-tools
270
+ append_system_prompt: |
271
+ Use tools precisely. Batch independent operations in parallel
272
+ when possible. Match Claude Sonnet's tool-call style: terse,
273
+ structured, no preamble before invoking a tool.
274
+ providers:
275
+ - openrouter-qwen-coder-free # primary: agentic coding 特化
276
+ - openrouter-gpt-oss-free # vendor diversity (rate-limit 逃げ)
277
+ - openrouter-gemini-flash-free # 1M ctx fallback
278
+
279
+ # 画像入力 (vision) 専用。Gemini Flash の 1M ctx で画像 + 長文も同時に。
280
+ - name: vision
281
+ append_system_prompt: |
282
+ Describe images factually with no speculation beyond what is visible.
283
+ Match Claude Sonnet's vision style: terse, accurate, no fluff.
284
+ providers:
285
+ - openrouter-gemini-flash-free # 1M ctx + vision native
286
+ - openrouter-qwen-coder-free # 視覚タスクは弱いが text fallback として
287
+ - openrouter-gpt-oss-free # text-only fallback
288
+
289
+ # 長尺コンテキスト (32K token 超) — Pi 上の 8K-32K ローカルでは詰むので
290
+ # クラウドの大きい ctx に逃がす。Gemini Flash 1M が現状ベスト。
291
+ - name: longcontext
292
+ append_system_prompt: |
293
+ The user is sending a long-context request. Read the entire input
294
+ carefully, structure your reasoning, and answer concisely.
295
+ providers:
296
+ - openrouter-gemini-flash-free # 1M ctx
297
+ - openrouter-qwen-coder-free # 262K ctx fallback
298
+ - openrouter-gpt-oss-free # 131K ctx fallback
@@ -11,7 +11,7 @@
11
11
  # in plan.md §11.B; once granted, this name will become an alias and
12
12
  # `coderouter` will become the canonical distribution name.
13
13
  name = "coderouter-cli"
14
- version = "1.10.0"
14
+ version = "1.10.1"
15
15
  description = "Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible."
16
16
  readme = "README.md"
17
17
  requires-python = ">=3.12"