coderouter-cli 2.9.2__py3-none-any.whl → 2.9.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/config/schemas.py +91 -6
- coderouter/ingress/launcher_routes.py +856 -6
- coderouter/launcher_devices.py +518 -0
- coderouter/launcher_swap.py +100 -12
- coderouter/translation/anthropic.py +26 -5
- {coderouter_cli-2.9.2.dist-info → coderouter_cli-2.9.4.dist-info}/METADATA +2 -2
- {coderouter_cli-2.9.2.dist-info → coderouter_cli-2.9.4.dist-info}/RECORD +10 -9
- {coderouter_cli-2.9.2.dist-info → coderouter_cli-2.9.4.dist-info}/WHEEL +0 -0
- {coderouter_cli-2.9.2.dist-info → coderouter_cli-2.9.4.dist-info}/entry_points.txt +0 -0
- {coderouter_cli-2.9.2.dist-info → coderouter_cli-2.9.4.dist-info}/licenses/LICENSE +0 -0
coderouter/launcher_swap.py
CHANGED
|
@@ -85,9 +85,18 @@ class _ModelState:
|
|
|
85
85
|
def _pick_ephemeral_port() -> int:
|
|
86
86
|
"""Ask the OS for a free port. Best-effort only (§6.6 known-trap #4):
|
|
87
87
|
|
|
88
|
-
there is an unavoidable TOCTOU window between this call
|
|
89
|
-
and the child actually binding
|
|
90
|
-
|
|
88
|
+
there is an unavoidable TOCTOU window between this call's bind+close
|
|
89
|
+
returning a "free" port and the child actually binding that same
|
|
90
|
+
port a moment later — nothing stops another process (or another
|
|
91
|
+
concurrent swap spawn) from grabbing it first. Retrying on a fresh
|
|
92
|
+
port when the child fails to come up
|
|
93
|
+
(``LauncherSwapConfig.port_retry_attempts``, default 2 additional
|
|
94
|
+
attempts) narrows the *impact* of losing that race but does NOT
|
|
95
|
+
close the window itself — a genuinely race-free allocation would
|
|
96
|
+
need the OS to hand the child an already-bound socket fd, which the
|
|
97
|
+
current spawn path (argv ``--port N``) does not support. Fixed
|
|
98
|
+
``port:`` in the catalog entry avoids the race entirely (§10 Q2,
|
|
99
|
+
recommended) and remains the only way to eliminate it outright.
|
|
91
100
|
"""
|
|
92
101
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
93
102
|
s.bind(("127.0.0.1", 0))
|
|
@@ -126,6 +135,13 @@ class SwapManager:
|
|
|
126
135
|
self._readiness_timeout_s = config.readiness_timeout_s
|
|
127
136
|
self._sweep_interval_s = config.sweep_interval_s
|
|
128
137
|
self._sweeper_task: asyncio.Task[None] | None = None
|
|
138
|
+
# [Unreleased] per-model TTL override: the sweeper must run
|
|
139
|
+
# whenever EITHER the global TTL is enabled OR at least one
|
|
140
|
+
# catalog entry sets its own ``ttl_seconds`` (even if the global
|
|
141
|
+
# value is None/disabled) — see _effective_ttl / start().
|
|
142
|
+
self._any_ttl_configured = self._ttl_seconds is not None or any(
|
|
143
|
+
m.ttl_seconds is not None for m in config.models
|
|
144
|
+
)
|
|
129
145
|
|
|
130
146
|
# ------------------------------------------------------------------
|
|
131
147
|
# Catalog matching
|
|
@@ -214,24 +230,40 @@ class SwapManager:
|
|
|
214
230
|
async with state.lock:
|
|
215
231
|
await self._unload_locked(state, spec, reason)
|
|
216
232
|
|
|
233
|
+
def _effective_ttl(self, spec: SwapModelSpec) -> float | None:
|
|
234
|
+
"""``spec.ttl_seconds`` if set, else the global ``self._ttl_seconds``.
|
|
235
|
+
|
|
236
|
+
[Unreleased] per-model TTL override (docs/backends/launcher.md
|
|
237
|
+
"launcher.swap fields" table). ``None`` at either level keeps
|
|
238
|
+
its usual meaning ("TTL disabled") — a spec-level override only
|
|
239
|
+
takes effect when it's not ``None``.
|
|
240
|
+
"""
|
|
241
|
+
return spec.ttl_seconds if spec.ttl_seconds is not None else self._ttl_seconds
|
|
242
|
+
|
|
217
243
|
async def sweep_once(self) -> None:
|
|
218
244
|
"""One TTL sweep pass: stop every idle, un-leased, expired model.
|
|
219
245
|
|
|
220
246
|
Takes each model's own lock independently (never more than one
|
|
221
247
|
per-model lock at a time — §6.6 known-trap #7) so a slow unload
|
|
222
|
-
for model A never delays the sweep of model B.
|
|
248
|
+
for model A never delays the sweep of model B. Each model's TTL
|
|
249
|
+
is resolved individually via :meth:`_effective_ttl` — a model
|
|
250
|
+
whose global TTL is disabled can still expire on its own
|
|
251
|
+
override, and vice versa.
|
|
223
252
|
"""
|
|
224
|
-
if self.
|
|
253
|
+
if not self._any_ttl_configured:
|
|
225
254
|
return
|
|
226
255
|
now = time.monotonic()
|
|
227
256
|
for spec in list(self._catalog.values()):
|
|
257
|
+
ttl = self._effective_ttl(spec)
|
|
258
|
+
if ttl is None:
|
|
259
|
+
continue
|
|
228
260
|
state = self._states.get(spec.name)
|
|
229
261
|
if state is None:
|
|
230
262
|
continue
|
|
231
263
|
async with state.lock:
|
|
232
264
|
if state.status != "ready" or state.in_flight > 0:
|
|
233
265
|
continue
|
|
234
|
-
if (now - state.last_used) <
|
|
266
|
+
if (now - state.last_used) < ttl:
|
|
235
267
|
continue
|
|
236
268
|
await self._unload_locked(state, spec, reason="ttl")
|
|
237
269
|
|
|
@@ -240,8 +272,14 @@ class SwapManager:
|
|
|
240
272
|
# ------------------------------------------------------------------
|
|
241
273
|
|
|
242
274
|
async def start(self) -> None:
|
|
243
|
-
"""Start the TTL sweeper background task.
|
|
244
|
-
|
|
275
|
+
"""Start the TTL sweeper background task.
|
|
276
|
+
|
|
277
|
+
No-op when TTL is disabled both globally AND on every catalog
|
|
278
|
+
entry (``self._any_ttl_configured`` — [Unreleased] per-model
|
|
279
|
+
override means a per-model ``ttl_seconds`` alone is enough to
|
|
280
|
+
need the sweeper, even with the global TTL off).
|
|
281
|
+
"""
|
|
282
|
+
if not self._any_ttl_configured or self._sweeper_task is not None:
|
|
245
283
|
return
|
|
246
284
|
self._sweeper_task = asyncio.create_task(self._sweeper_loop())
|
|
247
285
|
|
|
@@ -285,6 +323,35 @@ class SwapManager:
|
|
|
285
323
|
def _engine(self) -> Any | None:
|
|
286
324
|
return getattr(self._app.state, "engine", None)
|
|
287
325
|
|
|
326
|
+
def _remove_from_registry(self, proc_id: str) -> None:
|
|
327
|
+
"""Drop a swap-managed ``ManagedProcess`` from the launcher registry.
|
|
328
|
+
|
|
329
|
+
[Unreleased] registry-litter fix: ``stop_process`` only sets
|
|
330
|
+
``status="stopped"`` — it never removes the entry, because for
|
|
331
|
+
MANUALLY started processes the stopped row doubles as visible
|
|
332
|
+
history in the /launcher UI (with logs and an explicit ✕ delete
|
|
333
|
+
button). Swap-managed processes have no such contract: every
|
|
334
|
+
failed readiness attempt (times ``1 + port_retry_attempts``) and
|
|
335
|
+
every TTL unload would otherwise leave a permanent "stopped" row
|
|
336
|
+
accumulating unboundedly in ``GET /api/launcher/processes``. So
|
|
337
|
+
SwapManager removes ITS OWN processes right after it stops them,
|
|
338
|
+
guarded on ``swap_managed`` so a manual process can never be
|
|
339
|
+
swept up by mistake. Crash leftovers (a swap process that died
|
|
340
|
+
on its own, status "error"/"stopped" without SwapManager
|
|
341
|
+
stopping it) are deliberately NOT removed here — their log tail
|
|
342
|
+
is the only crash forensics an operator has; the ✕ button still
|
|
343
|
+
applies.
|
|
344
|
+
"""
|
|
345
|
+
reg = _registry_for_app(self._app)
|
|
346
|
+
try:
|
|
347
|
+
proc = reg.get(proc_id)
|
|
348
|
+
except KeyError:
|
|
349
|
+
return
|
|
350
|
+
if not proc.swap_managed:
|
|
351
|
+
return
|
|
352
|
+
with contextlib.suppress(KeyError):
|
|
353
|
+
reg.remove(proc_id)
|
|
354
|
+
|
|
288
355
|
def _resolve_options(self, spec: SwapModelSpec) -> dict[str, Any]:
|
|
289
356
|
"""Resolve ``spec.option_profile`` into the launcher options dict."""
|
|
290
357
|
if not spec.option_profile or self._launcher_cfg is None:
|
|
@@ -346,11 +413,20 @@ class SwapManager:
|
|
|
346
413
|
async def _spawn_with_retry(
|
|
347
414
|
self, spec: SwapModelSpec
|
|
348
415
|
) -> tuple[ManagedProcess | None, Exception | None]:
|
|
349
|
-
"""Spawn + wait for readiness.
|
|
350
|
-
|
|
351
|
-
|
|
416
|
+
"""Spawn + wait for readiness.
|
|
417
|
+
|
|
418
|
+
§10 Q2: when ``spec.port`` is unset, up to
|
|
419
|
+
``1 + LauncherSwapConfig.port_retry_attempts`` attempts total
|
|
420
|
+
(default: 1 initial + 2 retries = 3), each on a freshly picked
|
|
421
|
+
ephemeral port via :func:`_pick_ephemeral_port` — see that
|
|
422
|
+
function's docstring for the residual pick-then-bind TOCTOU
|
|
423
|
+
window this does NOT close, only bounds the impact of. Fixed
|
|
424
|
+
ports never retry — a second attempt on the same port would
|
|
425
|
+
just collide again.
|
|
352
426
|
"""
|
|
353
|
-
attempts =
|
|
427
|
+
attempts = (
|
|
428
|
+
1 if spec.port is not None else 1 + self._config.port_retry_attempts
|
|
429
|
+
)
|
|
354
430
|
last_exc: Exception | None = None
|
|
355
431
|
for _attempt in range(attempts):
|
|
356
432
|
port = spec.port if spec.port is not None else _pick_ephemeral_port()
|
|
@@ -368,6 +444,10 @@ class SwapManager:
|
|
|
368
444
|
)
|
|
369
445
|
with contextlib.suppress(Exception):
|
|
370
446
|
await stop_process(self._app, proc.id)
|
|
447
|
+
# [Unreleased] registry-litter fix: without this, every failed
|
|
448
|
+
# readiness attempt leaves one "stopped" row in the registry
|
|
449
|
+
# forever (1 + port_retry_attempts rows per failed load).
|
|
450
|
+
self._remove_from_registry(proc.id)
|
|
371
451
|
return None, last_exc
|
|
372
452
|
|
|
373
453
|
async def _spawn(self, spec: SwapModelSpec, port: int) -> ManagedProcess:
|
|
@@ -394,6 +474,10 @@ class SwapManager:
|
|
|
394
474
|
# provider). H-2: exempt from launcher auto-restart (crash
|
|
395
475
|
# recovery = next-request re-spawn under the per-model lock).
|
|
396
476
|
swap_managed=True,
|
|
477
|
+
# [Unreleased]: catalog model name, surfaced by
|
|
478
|
+
# GET /api/launcher/processes as "swap_model" so the /launcher
|
|
479
|
+
# UI can show which swap catalog entry a process backs.
|
|
480
|
+
swap_model=spec.name,
|
|
397
481
|
)
|
|
398
482
|
|
|
399
483
|
async def _await_ready(self, proc: ManagedProcess) -> bool:
|
|
@@ -457,6 +541,10 @@ class SwapManager:
|
|
|
457
541
|
with contextlib.suppress(Exception):
|
|
458
542
|
await stop_process(self._app, proc_id)
|
|
459
543
|
await self._deregister(spec)
|
|
544
|
+
# [Unreleased] registry-litter fix: a TTL-unloaded swap process
|
|
545
|
+
# is not "history" the way a manually stopped one is — leaving
|
|
546
|
+
# it would grow the registry by one row per load/unload cycle.
|
|
547
|
+
self._remove_from_registry(proc_id)
|
|
460
548
|
logger.info(
|
|
461
549
|
"swap-unload",
|
|
462
550
|
extra={"model": spec.name, "reason": reason, "proc_id": proc_id},
|
|
@@ -143,9 +143,18 @@ def normalize_message_roles(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
143
143
|
(see anthropics/claude-code#63469, vllm-project/vllm#44000).
|
|
144
144
|
|
|
145
145
|
Policy:
|
|
146
|
-
- ``role: "system"``
|
|
147
|
-
|
|
148
|
-
same join rule as
|
|
146
|
+
- leading ``role: "system"`` (before any user/assistant turn) →
|
|
147
|
+
text content merged into the top-level ``system`` field
|
|
148
|
+
(appended after any existing system prompt; same join rule as
|
|
149
|
+
``convert.to_anthropic_request``). This is a genuine system
|
|
150
|
+
prompt from an OpenAI-style client, and it stays at position 0.
|
|
151
|
+
- mid-conversation ``role: "system"`` → coerced to ``user`` **in
|
|
152
|
+
place**, like any other non-spec role. Claude Code's
|
|
153
|
+
system-reminders are volatile per-turn text; hoisting them to
|
|
154
|
+
the top-level field moves content that changes every turn ahead
|
|
155
|
+
of the whole conversation, which invalidates the prefix cache of
|
|
156
|
+
local backends (llama.cpp / LM Studio) and forces a full prompt
|
|
157
|
+
reprocess on every request.
|
|
149
158
|
- any other non-spec role (``ctx``, ``msg``, ...) → coerced to
|
|
150
159
|
``user`` so conversation position is preserved. Anthropic
|
|
151
160
|
merges consecutive same-role turns, so this is safe.
|
|
@@ -175,9 +184,21 @@ def normalize_message_roles(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
175
184
|
continue
|
|
176
185
|
if role == "system":
|
|
177
186
|
text = _content_as_text(msg.get("content"))
|
|
178
|
-
if text:
|
|
179
|
-
system_texts.append(text)
|
|
180
187
|
coerced_roles.append("system")
|
|
188
|
+
if text:
|
|
189
|
+
if not messages_out:
|
|
190
|
+
# Leading system message — a genuine system prompt from an
|
|
191
|
+
# OpenAI-style client. Hoisting to the top-level field is
|
|
192
|
+
# correct and prefix-safe (it stays at position 0).
|
|
193
|
+
system_texts.append(text)
|
|
194
|
+
else:
|
|
195
|
+
# Mid-conversation system message — volatile per-turn text
|
|
196
|
+
# (Claude Code >= 2.1.154 system-reminders). Keep it where
|
|
197
|
+
# the client put it. Hoisting moves text that changes every
|
|
198
|
+
# turn ahead of the entire conversation, which invalidates
|
|
199
|
+
# the prefix cache of local backends (llama.cpp / LM Studio)
|
|
200
|
+
# and forces a full prompt reprocess on every request.
|
|
201
|
+
messages_out.append({"role": "user", "content": text})
|
|
181
202
|
continue
|
|
182
203
|
# Unknown role (ctx / msg / future surprises): keep its position
|
|
183
204
|
# in the conversation as a user turn; drop if nothing salvageable.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.9.
|
|
3
|
+
Version: 2.9.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
|
|
@@ -40,7 +40,7 @@ Requires-Dist: types-pyyaml>=6.0.12; extra == 'dev'
|
|
|
40
40
|
Provides-Extra: doctor
|
|
41
41
|
Requires-Dist: ruamel-yaml>=0.18.6; extra == 'doctor'
|
|
42
42
|
Provides-Extra: repair
|
|
43
|
-
Requires-Dist: json-repair>=0.
|
|
43
|
+
Requires-Dist: json-repair>=0.60.1; extra == 'repair'
|
|
44
44
|
Description-Content-Type: text/markdown
|
|
45
45
|
|
|
46
46
|
<h1 align="center">CodeRouter</h1>
|
|
@@ -10,8 +10,9 @@ coderouter/errors.py,sha256=Xmq67lheyw8iv3Ox39jh2c4tvNI5RcUR4QkoxVDN6l4,1130
|
|
|
10
10
|
coderouter/gguf_introspect.py,sha256=KeE00CfbYRa4gSTrzuwC6zVuHi20IzLC4nUQa98BEXI,10387
|
|
11
11
|
coderouter/hardware.py,sha256=gn3_9qbVcGRR81yKMn1lJE_8-YDRau0LxIH_M-f7pxE,8356
|
|
12
12
|
coderouter/language_tax.py,sha256=LTbE3tIfoJuV2O3T0NixRKhzq_dEOTUuPEerJv2q9uk,9360
|
|
13
|
+
coderouter/launcher_devices.py,sha256=SoGpzknRqvnmvg_IeXy1TkeE7aLc_MOFqLzXdd3CjVs,19275
|
|
13
14
|
coderouter/launcher_speculative.py,sha256=kWaHNmhzBsbWuM_lRGDlbWkgJ1suuxBWQNTKhJOC-yg,12644
|
|
14
|
-
coderouter/launcher_swap.py,sha256=
|
|
15
|
+
coderouter/launcher_swap.py,sha256=W7qpdB-8Npr8yHGfx0N3SgZ2vDJ6BR9JuxW0I3TaZ-4,23783
|
|
15
16
|
coderouter/logging.py,sha256=5rfi4_R4aoeeUYDc642dgs61wA0aBX7wvubyB-k3Uyo,58074
|
|
16
17
|
coderouter/output_filters.py,sha256=0ry_rPiS_kC-FnHgaNVP6v7e6Al2djxzu9vBzZ8kEkE,25314
|
|
17
18
|
coderouter/token_estimation.py,sha256=iz22vZEEW2P7uKLB2pYvPNpIbZGbgXRO5MtfkS_-9Sk,7531
|
|
@@ -25,7 +26,7 @@ coderouter/config/__init__.py,sha256=FODEn74fN-qZnt4INPSHswqhOlEgpL6-_onxsitSx8g
|
|
|
25
26
|
coderouter/config/capability_registry.py,sha256=RZgzHF54dXy0CCDocRCz2ee21qXGbbgTSKTkRI9iLL4,16936
|
|
26
27
|
coderouter/config/env_file.py,sha256=CoMK27fuAXm-NtoLzXb8yN2E-wDFjHQuFwiIlmgTBQw,10356
|
|
27
28
|
coderouter/config/loader.py,sha256=FUEe8m4Tnmj_aul0vSctD8vKvNW-oLRoMRbTpSKqSmc,4077
|
|
28
|
-
coderouter/config/schemas.py,sha256=
|
|
29
|
+
coderouter/config/schemas.py,sha256=wHaOqbehppjtL-01eLh8DNmm0A3rZXdJ-cNbBcS2yM8,120984
|
|
29
30
|
coderouter/data/__init__.py,sha256=uNyfD9jaCvTWsBAWtaw1Fr25OSxzv3psGMfBjT1z0Cc,328
|
|
30
31
|
coderouter/data/model-capabilities.yaml,sha256=S9jt6SC6-3s2-icZ_n-a14iEMnc2yB1C2R6q-N_tZWQ,19309
|
|
31
32
|
coderouter/guards/__init__.py,sha256=5qliYBqygvVPneej7nx0uSjxDKsz7t8VzvrDgVBJlvU,1170
|
|
@@ -43,7 +44,7 @@ coderouter/ingress/__init__.py,sha256=WQsCH2CGJCAhy0mS6GSEdeYZRkkQu2OHDsP4CJWTLu
|
|
|
43
44
|
coderouter/ingress/anthropic_routes.py,sha256=1ThXzx8VgxRV7UCi3NF5ey7kbr0ADPKTXyQuYS5gK1w,27063
|
|
44
45
|
coderouter/ingress/app.py,sha256=6e_qNlYZIyjLKhY20t0ZNL-x5Fv8R8oaIH_HCdxa9Ic,20921
|
|
45
46
|
coderouter/ingress/dashboard_routes.py,sha256=1SqNNrVTfQiZcx5n2MvjAzWirittX0e61tCt-MKp4Ag,25617
|
|
46
|
-
coderouter/ingress/launcher_routes.py,sha256=
|
|
47
|
+
coderouter/ingress/launcher_routes.py,sha256=lU2EILVAi-lvHBHIxyixsMTRZlp-Rv7-2sw-eW4ccYg,119687
|
|
47
48
|
coderouter/ingress/metrics_routes.py,sha256=M22dwOGn24P05Ge4W3c7d7mYytSGWjIR-pPSPOAiHJY,3965
|
|
48
49
|
coderouter/ingress/openai_routes.py,sha256=TrugsQNJfNPgKKLzfT1aXzrs4emWYGE4XebEBuaZoWk,12505
|
|
49
50
|
coderouter/metrics/__init__.py,sha256=7Es351DPS7yLM0yVF_F0eesmiD83n7Zzhie44chht38,1465
|
|
@@ -66,11 +67,11 @@ coderouter/state/request_log.py,sha256=mNxQJ9IDGpUtT5rZbCo_-CTyhxmvig7C2Gfvdn9rc
|
|
|
66
67
|
coderouter/state/store.py,sha256=h-rsMJq8GILsOfCP94nI40cuHaj4Vqycsm9UNN77REI,7445
|
|
67
68
|
coderouter/state/suggest_rules.py,sha256=FvdhEvao5NvdKp9zs8AkcoFKHY4yqqXY2HekvSjpDFA,16670
|
|
68
69
|
coderouter/translation/__init__.py,sha256=PYXN7XVEwpG1uC8RLy6fvnGbzEZhhrEuUapH8IYOtG8,1788
|
|
69
|
-
coderouter/translation/anthropic.py,sha256=
|
|
70
|
+
coderouter/translation/anthropic.py,sha256=uEScq-icLn6ZXFWaxsKROv7feaq3zD0ZogjbTJ3Y7gQ,12578
|
|
70
71
|
coderouter/translation/convert.py,sha256=VV4kpu4dvLmGBmJfQnCLY5ryy_AnQcV2NuwjqRtaR8Q,52633
|
|
71
72
|
coderouter/translation/tool_repair.py,sha256=DGOKArM53yfVbIfiAveoul1vGq9NEKmjagf43Qh4IkM,57630
|
|
72
|
-
coderouter_cli-2.9.
|
|
73
|
-
coderouter_cli-2.9.
|
|
74
|
-
coderouter_cli-2.9.
|
|
75
|
-
coderouter_cli-2.9.
|
|
76
|
-
coderouter_cli-2.9.
|
|
73
|
+
coderouter_cli-2.9.4.dist-info/METADATA,sha256=E7EiSxxEYfkchQLvqlvdQzADFsnwbReGwi0Patjfk8I,15996
|
|
74
|
+
coderouter_cli-2.9.4.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
75
|
+
coderouter_cli-2.9.4.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
|
|
76
|
+
coderouter_cli-2.9.4.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
|
|
77
|
+
coderouter_cli-2.9.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|