coderouter-cli 2.7.3__py3-none-any.whl → 2.7.4__py3-none-any.whl
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/ingress/launcher_routes.py +51 -1
- coderouter/ingress/openai_routes.py +85 -8
- coderouter/routing/fallback.py +74 -0
- {coderouter_cli-2.7.3.dist-info → coderouter_cli-2.7.4.dist-info}/METADATA +3 -1
- {coderouter_cli-2.7.3.dist-info → coderouter_cli-2.7.4.dist-info}/RECORD +8 -8
- {coderouter_cli-2.7.3.dist-info → coderouter_cli-2.7.4.dist-info}/WHEEL +0 -0
- {coderouter_cli-2.7.3.dist-info → coderouter_cli-2.7.4.dist-info}/entry_points.txt +0 -0
- {coderouter_cli-2.7.3.dist-info → coderouter_cli-2.7.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -655,6 +655,28 @@ async def api_backends(request: Request) -> dict[str, Any]:
|
|
|
655
655
|
return {"backends": result}
|
|
656
656
|
|
|
657
657
|
|
|
658
|
+
def _launcher_provider_config(backend: str, port: int) -> Any:
|
|
659
|
+
"""Build the provider entry for a launcher-started backend.
|
|
660
|
+
|
|
661
|
+
``model`` is left EMPTY on purpose: llama-server / vllm / mlx decide
|
|
662
|
+
which model is actually loaded, and the empty-model /v1/models
|
|
663
|
+
passthrough then surfaces the upstream's real model id to benchmark
|
|
664
|
+
clients — swap the GGUF, no config edit needed.
|
|
665
|
+
|
|
666
|
+
Name embeds backend + port (``launcher-llamacpp-8085``) so restarting
|
|
667
|
+
on the same port REPLACES the entry instead of piling up duplicates.
|
|
668
|
+
"""
|
|
669
|
+
from coderouter.config.schemas import ProviderConfig
|
|
670
|
+
|
|
671
|
+
safe_backend = backend.replace(".", "")
|
|
672
|
+
return ProviderConfig(
|
|
673
|
+
name=f"launcher-{safe_backend}-{port}",
|
|
674
|
+
base_url=f"http://localhost:{port}/v1",
|
|
675
|
+
model="",
|
|
676
|
+
timeout_s=120.0,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
|
|
658
680
|
@router.post("/api/launcher/start")
|
|
659
681
|
async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
|
|
660
682
|
"""Start a new backend process."""
|
|
@@ -717,7 +739,35 @@ async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
|
|
|
717
739
|
_background_tasks.add(_task)
|
|
718
740
|
_task.add_done_callback(_background_tasks.discard)
|
|
719
741
|
|
|
720
|
-
|
|
742
|
+
# Provider auto-sync: register the just-started backend as a routable
|
|
743
|
+
# provider so requests can reach it without hand-editing providers.yaml
|
|
744
|
+
# (in-memory only — see FallbackEngine.register_provider docstring).
|
|
745
|
+
# Sync failure must never fail the start itself.
|
|
746
|
+
provider_sync: dict[str, Any] | None = None
|
|
747
|
+
engine = getattr(request.app.state, "engine", None)
|
|
748
|
+
if engine is not None and hasattr(engine, "register_provider"):
|
|
749
|
+
try:
|
|
750
|
+
provider_sync = engine.register_provider(
|
|
751
|
+
_launcher_provider_config(req.backend, req.port)
|
|
752
|
+
)
|
|
753
|
+
proc.log_tail.append(
|
|
754
|
+
"[launcher] provider sync: "
|
|
755
|
+
f"{provider_sync['provider']} -> profile "
|
|
756
|
+
f"'{provider_sync['profile']}' (in-memory)"
|
|
757
|
+
)
|
|
758
|
+
except Exception as exc:
|
|
759
|
+
logger.warning(
|
|
760
|
+
"launcher provider sync failed",
|
|
761
|
+
extra={"backend": req.backend, "port": req.port, "error": str(exc)},
|
|
762
|
+
)
|
|
763
|
+
proc.log_tail.append(f"[launcher] provider sync failed: {exc}")
|
|
764
|
+
|
|
765
|
+
return {
|
|
766
|
+
"id": proc_id,
|
|
767
|
+
"pid": p.pid,
|
|
768
|
+
"command": cmd,
|
|
769
|
+
"provider_sync": provider_sync,
|
|
770
|
+
}
|
|
721
771
|
|
|
722
772
|
|
|
723
773
|
@router.post("/api/launcher/stop/{proc_id}")
|
|
@@ -78,22 +78,99 @@ def _resolve_stream_timeout_s(engine: FallbackEngine, profile: str | None) -> fl
|
|
|
78
78
|
return max(_STREAM_TIMEOUT_MIN_S, float(per_call) * _STREAM_TIMEOUT_MULTIPLIER)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
# --- /v1/models upstream passthrough -------------------------------------
|
|
82
|
+
#
|
|
83
|
+
# Providers with an *empty* ``model`` field are passthrough providers: the
|
|
84
|
+
# upstream (llama-server, LM Studio, ...) decides which model is loaded and
|
|
85
|
+
# CodeRouter never sends a model name. For those providers the historic
|
|
86
|
+
# behaviour — returning the provider *name* — makes external benchmarks
|
|
87
|
+
# indistinguishable across loaded GGUFs (the launcher_gui setup always
|
|
88
|
+
# reported ``llama-cpp-local`` regardless of the selected model). We now ask
|
|
89
|
+
# the upstream's own ``/models`` endpoint for its real model id(s) and
|
|
90
|
+
# surface those instead.
|
|
91
|
+
#
|
|
92
|
+
# Guards: only for ``kind == "openai_compat"`` with ``model == ""``; a short
|
|
93
|
+
# per-upstream TTL cache keeps repeated SDK probes cheap; ANY failure (refused
|
|
94
|
+
# connection, timeout, unexpected shape) falls back to the historic
|
|
95
|
+
# provider-name entry, so this is strictly additive. Providers with a
|
|
96
|
+
# configured model keep the exact previous behaviour.
|
|
97
|
+
|
|
98
|
+
_UPSTREAM_MODELS_TTL_S = 30.0
|
|
99
|
+
_UPSTREAM_MODELS_TIMEOUT_S = 2.0
|
|
100
|
+
# base_url -> (expires_monotonic, ids)
|
|
101
|
+
_upstream_models_cache: dict[str, tuple[float, list[str]]] = {}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def _fetch_upstream_model_ids(base_url: str) -> list[str]:
|
|
105
|
+
"""GET {base_url}/models and return the upstream model ids.
|
|
106
|
+
|
|
107
|
+
Raises on any transport / shape problem — the caller treats every
|
|
108
|
+
exception as "fall back to the provider-name entry".
|
|
109
|
+
"""
|
|
110
|
+
import httpx # local import keeps module import time flat
|
|
111
|
+
|
|
112
|
+
url = base_url.rstrip("/") + "/models"
|
|
113
|
+
async with httpx.AsyncClient(timeout=_UPSTREAM_MODELS_TIMEOUT_S) as client:
|
|
114
|
+
resp = await client.get(url)
|
|
115
|
+
resp.raise_for_status()
|
|
116
|
+
payload = resp.json()
|
|
117
|
+
ids = [
|
|
118
|
+
str(item["id"])
|
|
119
|
+
for item in payload.get("data", [])
|
|
120
|
+
if isinstance(item, dict) and item.get("id")
|
|
121
|
+
]
|
|
122
|
+
if not ids:
|
|
123
|
+
raise ValueError(f"upstream {url} returned no model ids")
|
|
124
|
+
return ids
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def _upstream_model_ids_cached(base_url: str) -> list[str]:
|
|
128
|
+
"""TTL-cached wrapper around :func:`_fetch_upstream_model_ids`."""
|
|
129
|
+
now = time.monotonic()
|
|
130
|
+
hit = _upstream_models_cache.get(base_url)
|
|
131
|
+
if hit is not None and hit[0] > now:
|
|
132
|
+
return hit[1]
|
|
133
|
+
ids = await _fetch_upstream_model_ids(base_url)
|
|
134
|
+
_upstream_models_cache[base_url] = (now + _UPSTREAM_MODELS_TTL_S, ids)
|
|
135
|
+
return ids
|
|
136
|
+
|
|
137
|
+
|
|
81
138
|
@router.get("/models")
|
|
82
139
|
async def list_models(request: Request) -> dict[str, object]:
|
|
83
|
-
"""
|
|
140
|
+
"""/v1/models: provider names, with upstream passthrough for empty-model
|
|
141
|
+
providers (see the passthrough note above)."""
|
|
84
142
|
config = request.app.state.config
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
143
|
+
created = int(time.time())
|
|
144
|
+
data: list[dict[str, object]] = []
|
|
145
|
+
for p in config.providers:
|
|
146
|
+
if p.model == "" and p.kind == "openai_compat":
|
|
147
|
+
try:
|
|
148
|
+
ids = await _upstream_model_ids_cached(str(p.base_url))
|
|
149
|
+
except Exception: # any failure means "no passthrough"
|
|
150
|
+
logger.debug(
|
|
151
|
+
"models passthrough failed; falling back to provider name",
|
|
152
|
+
extra={"provider": p.name},
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
data.extend(
|
|
156
|
+
{
|
|
157
|
+
"id": model_id,
|
|
158
|
+
"object": "model",
|
|
159
|
+
"created": created,
|
|
160
|
+
"owned_by": f"coderouter/{p.name}",
|
|
161
|
+
}
|
|
162
|
+
for model_id in ids
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
data.append(
|
|
88
166
|
{
|
|
89
167
|
"id": p.name,
|
|
90
168
|
"object": "model",
|
|
91
|
-
"created":
|
|
169
|
+
"created": created,
|
|
92
170
|
"owned_by": "coderouter",
|
|
93
171
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
172
|
+
)
|
|
173
|
+
return {"object": "list", "data": data}
|
|
97
174
|
|
|
98
175
|
|
|
99
176
|
@router.post("/chat/completions", response_model=None)
|
coderouter/routing/fallback.py
CHANGED
|
@@ -1173,6 +1173,80 @@ class FallbackEngine:
|
|
|
1173
1173
|
# v2.0-K: persistent state store (None = in-memory only).
|
|
1174
1174
|
self._state_store: StateStore | None = None
|
|
1175
1175
|
|
|
1176
|
+
def register_provider(
|
|
1177
|
+
self, provider: ProviderConfig, profile_name: str = "launcher"
|
|
1178
|
+
) -> dict[str, Any]:
|
|
1179
|
+
"""Register (or update) a provider at runtime — launcher auto-sync.
|
|
1180
|
+
|
|
1181
|
+
Motivation: the embedded launcher can start a llama.cpp / vllm /
|
|
1182
|
+
mlx backend on an arbitrary port, but routing is driven entirely
|
|
1183
|
+
by providers.yaml — historically the operator had to hand-edit the
|
|
1184
|
+
config or the new backend was simply unreachable (observed in the
|
|
1185
|
+
wild as "launcher started on 8085, providers.yaml pointed at
|
|
1186
|
+
8080"). This method closes that gap in-process:
|
|
1187
|
+
|
|
1188
|
+
- the provider entry is appended to ``config.providers`` (or, when
|
|
1189
|
+
a provider with the same *name* already exists, its config entry
|
|
1190
|
+
and cached adapter are REPLACED — the launcher restarting a
|
|
1191
|
+
backend on the same port lands here),
|
|
1192
|
+
- an adapter is built and cached alongside the static ones,
|
|
1193
|
+
- the ``profile_name`` chain is created on demand, and the
|
|
1194
|
+
provider is moved to the FRONT of that chain (most recently
|
|
1195
|
+
launched backend wins the launcher profile).
|
|
1196
|
+
|
|
1197
|
+
Deliberately **in-memory only** — no providers.yaml rewrite. A
|
|
1198
|
+
YAML round-trip would destroy operator comments (and a
|
|
1199
|
+
comment-preserving writer means a new dependency), and the
|
|
1200
|
+
launcher's process registry is itself in-memory by design: both
|
|
1201
|
+
the process and its provider entry share the lifetime of this
|
|
1202
|
+
server process. ``default_profile`` is never touched — routing to
|
|
1203
|
+
the launcher profile stays an explicit opt-in
|
|
1204
|
+
(``X-CodeRouter-Profile: launcher`` or body ``profile``).
|
|
1205
|
+
|
|
1206
|
+
Returns a small summary dict for the launcher API response.
|
|
1207
|
+
"""
|
|
1208
|
+
from coderouter.config.schemas import FallbackChain
|
|
1209
|
+
|
|
1210
|
+
replaced = False
|
|
1211
|
+
for i, existing in enumerate(self.config.providers):
|
|
1212
|
+
if existing.name == provider.name:
|
|
1213
|
+
self.config.providers[i] = provider
|
|
1214
|
+
replaced = True
|
|
1215
|
+
break
|
|
1216
|
+
if not replaced:
|
|
1217
|
+
self.config.providers.append(provider)
|
|
1218
|
+
self._adapters[provider.name] = build_adapter(provider)
|
|
1219
|
+
|
|
1220
|
+
try:
|
|
1221
|
+
chain = self.config.profile_by_name(profile_name)
|
|
1222
|
+
except KeyError:
|
|
1223
|
+
chain = FallbackChain(name=profile_name, providers=[provider.name])
|
|
1224
|
+
self.config.profiles.append(chain)
|
|
1225
|
+
else:
|
|
1226
|
+
if provider.name in chain.providers:
|
|
1227
|
+
chain.providers.remove(provider.name)
|
|
1228
|
+
chain.providers.insert(0, provider.name)
|
|
1229
|
+
|
|
1230
|
+
logger.info(
|
|
1231
|
+
"launcher provider sync: registered %s -> %s (profile %r)",
|
|
1232
|
+
provider.name,
|
|
1233
|
+
provider.base_url,
|
|
1234
|
+
profile_name,
|
|
1235
|
+
extra={
|
|
1236
|
+
"provider": provider.name,
|
|
1237
|
+
"base_url": str(provider.base_url),
|
|
1238
|
+
"profile": profile_name,
|
|
1239
|
+
"replaced": replaced,
|
|
1240
|
+
},
|
|
1241
|
+
)
|
|
1242
|
+
return {
|
|
1243
|
+
"provider": provider.name,
|
|
1244
|
+
"base_url": str(provider.base_url),
|
|
1245
|
+
"profile": profile_name,
|
|
1246
|
+
"replaced": replaced,
|
|
1247
|
+
"persisted": False, # in-memory only, by design (see docstring)
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1176
1250
|
@property
|
|
1177
1251
|
def plugins(self) -> PluginRegistry:
|
|
1178
1252
|
"""Return the plugin registry, lazily building an empty one if absent.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.4
|
|
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
|
|
@@ -220,6 +220,8 @@ providers:
|
|
|
220
220
|
| **オプションプロファイル** | `providers.yaml` に名前付きプリセットを定義 → ドロップダウンで選択するだけ |
|
|
221
221
|
| **複数プロセス管理** | llama.cpp と vllm を同時に起動し、ポートごとに独立管理 |
|
|
222
222
|
| **ログビューア** | 各プロセスの stdout/stderr をブラウザ内でリアルタイム確認 |
|
|
223
|
+
| **provider 自動同期** (v2.7.4) | 起動したバックエンドを provider として自動登録(`launcher-llamacpp-8085` 等)。providers.yaml 無編集で `X-CodeRouter-Profile: launcher` からルーティング可能。メモリ内のみ・serve と同寿命 |
|
|
224
|
+
| **モデル名パススルー** (v2.7.4) | `model: ""` の provider は `/v1/models` が上流のロード中モデル ID(gguf 名)をそのまま返す。gguf を差し替えても config 編集不要 — 外部ベンチからモデルを識別できる |
|
|
223
225
|
|
|
224
226
|
```yaml
|
|
225
227
|
# providers.yaml に追記するだけで有効になる
|
|
@@ -41,9 +41,9 @@ coderouter/ingress/__init__.py,sha256=WQsCH2CGJCAhy0mS6GSEdeYZRkkQu2OHDsP4CJWTLu
|
|
|
41
41
|
coderouter/ingress/anthropic_routes.py,sha256=1ThXzx8VgxRV7UCi3NF5ey7kbr0ADPKTXyQuYS5gK1w,27063
|
|
42
42
|
coderouter/ingress/app.py,sha256=h6s-UWsbGP22rku4pPVlvRdrtbMj4KoEEUZoQQOrTTw,19370
|
|
43
43
|
coderouter/ingress/dashboard_routes.py,sha256=1SqNNrVTfQiZcx5n2MvjAzWirittX0e61tCt-MKp4Ag,25617
|
|
44
|
-
coderouter/ingress/launcher_routes.py,sha256=
|
|
44
|
+
coderouter/ingress/launcher_routes.py,sha256=QzG0l0c2ZmAcQai3E-xs3cV5fUYf3ajjZauvI9pKDh4,57959
|
|
45
45
|
coderouter/ingress/metrics_routes.py,sha256=M22dwOGn24P05Ge4W3c7d7mYytSGWjIR-pPSPOAiHJY,3965
|
|
46
|
-
coderouter/ingress/openai_routes.py,sha256=
|
|
46
|
+
coderouter/ingress/openai_routes.py,sha256=TrugsQNJfNPgKKLzfT1aXzrs4emWYGE4XebEBuaZoWk,12505
|
|
47
47
|
coderouter/metrics/__init__.py,sha256=7Es351DPS7yLM0yVF_F0eesmiD83n7Zzhie44chht38,1465
|
|
48
48
|
coderouter/metrics/collector.py,sha256=wdGuX5yOBWEHCz1KVIfaUBZP5NXW3U34yyYlLGwgoCE,61212
|
|
49
49
|
coderouter/metrics/prometheus.py,sha256=THYkvrcpQK3ZNW-BV0h3O-PoT2ppNZ0dapQuu1nyHwE,24410
|
|
@@ -56,7 +56,7 @@ coderouter/routing/adaptive.py,sha256=G2o377twGSjbUh65wiIFx6klnpFGjsD_nI3oDvcBwh
|
|
|
56
56
|
coderouter/routing/auto_router.py,sha256=y4v0c8u5F9f98Vmhx1vRcKPiOgAvpzbFqr6TIh058h0,13341
|
|
57
57
|
coderouter/routing/budget.py,sha256=PblmVKJGs_BwNa9uDHAA8hmZ4XIVKv38mHAeU0V3OMs,8451
|
|
58
58
|
coderouter/routing/capability.py,sha256=rRQhzTgQYFTFDNYcSLKvq3xjdw6B1w7T-3jS7PTMI14,27864
|
|
59
|
-
coderouter/routing/fallback.py,sha256=
|
|
59
|
+
coderouter/routing/fallback.py,sha256=L5hwe76DRlmHTR39dXafi27T5zRrgLlgX9uINddcSGk,142983
|
|
60
60
|
coderouter/state/__init__.py,sha256=XoGcPmmBQSiZWML2S0juSveQ78xfhtdeCliNnVyzu7E,1088
|
|
61
61
|
coderouter/state/audit_log.py,sha256=n7vuDsTfd5iuNUJhlRQPakJ2tMSsT9EfgPCzs1GEaac,10941
|
|
62
62
|
coderouter/state/replay.py,sha256=Z_YHKroTKZdrL8qObFxcoLOAQWWXZvXFdLfxzvBhEJg,11230
|
|
@@ -67,8 +67,8 @@ coderouter/translation/__init__.py,sha256=PYXN7XVEwpG1uC8RLy6fvnGbzEZhhrEuUapH8I
|
|
|
67
67
|
coderouter/translation/anthropic.py,sha256=aZkcYH4x82b0x7efJgJb9RWn9Hbyc9pEOthXe4vjUdU,11113
|
|
68
68
|
coderouter/translation/convert.py,sha256=VV4kpu4dvLmGBmJfQnCLY5ryy_AnQcV2NuwjqRtaR8Q,52633
|
|
69
69
|
coderouter/translation/tool_repair.py,sha256=DGOKArM53yfVbIfiAveoul1vGq9NEKmjagf43Qh4IkM,57630
|
|
70
|
-
coderouter_cli-2.7.
|
|
71
|
-
coderouter_cli-2.7.
|
|
72
|
-
coderouter_cli-2.7.
|
|
73
|
-
coderouter_cli-2.7.
|
|
74
|
-
coderouter_cli-2.7.
|
|
70
|
+
coderouter_cli-2.7.4.dist-info/METADATA,sha256=55ucKwRohGC6WEQr_NyGWjRjzhVgE7rOYgl4rfDNNkw,15388
|
|
71
|
+
coderouter_cli-2.7.4.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
72
|
+
coderouter_cli-2.7.4.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
|
|
73
|
+
coderouter_cli-2.7.4.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
|
|
74
|
+
coderouter_cli-2.7.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|