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.
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/CHANGELOG.md +135 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/PKG-INFO +1 -1
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/config/schemas.py +25 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/routing/auto_router.py +55 -8
- coderouter_cli-1.10.1/examples/providers.raspberrypi.yaml +298 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/pyproject.toml +1 -1
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_auto_router.py +216 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/.gitignore +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/LICENSE +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/README.en.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/README.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/__init__.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/__main__.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/adapters/anthropic_native.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/adapters/base.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/adapters/openai_compat.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/cli.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/config/loader.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/cost.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/doctor.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/env_security.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/errors.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/ingress/anthropic_routes.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/ingress/openai_routes.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/logging.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/output_filters.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/routing/capability.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/routing/fallback.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/translation/convert.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/free-tier-guide.en.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/free-tier-guide.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/hf-ollama-models.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/llamacpp-direct.en.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/llamacpp-direct.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/lmstudio-direct.en.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/lmstudio-direct.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/quickstart.en.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/quickstart.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/security.en.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/security.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/troubleshooting.en.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/troubleshooting.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/usage-guide.en.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/usage-guide.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/docs/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/examples/.env.example +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/examples/providers.auto-custom.yaml +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/examples/providers.auto.yaml +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/examples/providers.note-2026.yaml +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/examples/providers.yaml +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/__init__.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/conftest.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_backend_health.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_budget.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_capability.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_cli.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_config.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_doctor.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_env_file.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_env_security.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_errors.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_output_filters.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-1.10.0 → coderouter_cli-1.10.1}/tests/test_translation_anthropic.py +0 -0
- {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.
|
|
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 ``
|
|
282
|
-
# fire even without a user message (e.g. system-only
|
|
283
|
-
# request body that carries only a model field
|
|
284
|
-
# still require ``user_msg`` to be
|
|
285
|
-
# ``_match_rule``'s message-None
|
|
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.
|
|
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"
|