coderouter-cli 2.9.2__py3-none-any.whl → 2.9.3__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.
@@ -1518,10 +1518,25 @@ class SwapModelSpec(BaseModel):
1518
1518
  le=65535,
1519
1519
  description=(
1520
1520
  "Fixed port (recommended — §10 Q2). When unset, the swap "
1521
- "manager picks an OS-assigned ephemeral port and retries "
1522
- "once on a different port if the backend fails to become "
1523
- "ready. Best-effort only — no strong TOCTOU guarantee "
1524
- "(§6.6 known-trap #4)."
1521
+ "manager picks an OS-assigned ephemeral port and retries, "
1522
+ "each time on a freshly picked port, up to "
1523
+ "``LauncherSwapConfig.port_retry_attempts`` additional "
1524
+ "times if the backend fails to become ready. Best-effort "
1525
+ "only — no strong TOCTOU guarantee (§6.6 known-trap #4)."
1526
+ ),
1527
+ )
1528
+ ttl_seconds: float | None = Field(
1529
+ default=None,
1530
+ ge=0.0,
1531
+ description=(
1532
+ "[Unreleased] Per-model override of "
1533
+ "``LauncherSwapConfig.ttl_seconds``. ``None`` (default) = "
1534
+ "use the global value. ``0`` = unload as soon as the last "
1535
+ "in-flight lease for THIS model releases — same meaning as "
1536
+ "the global field's ``0``, just scoped to one catalog "
1537
+ "entry. Lets a large/expensive model unload sooner (or "
1538
+ "stay resident longer) than the rest of the catalog "
1539
+ "without changing the global default."
1525
1540
  ),
1526
1541
  )
1527
1542
  option_profile: str | None = Field(
@@ -1611,8 +1626,10 @@ class LauncherSwapConfig(BaseModel):
1611
1626
  "Seconds of no in-flight requests after which an idle swap "
1612
1627
  "process is auto-stopped. None = TTL disabled (runs until "
1613
1628
  "explicitly stopped). 0 = unload as soon as the last "
1614
- "in-flight lease releases. §10 Q1: one GLOBAL value for "
1615
- "Phase 1 no per-model override yet."
1629
+ "in-flight lease releases. §10 Q1: this is the GLOBAL "
1630
+ "default; a catalog entry's ``SwapModelSpec.ttl_seconds`` "
1631
+ "overrides it for that one model when set "
1632
+ "([Unreleased] per-model TTL override)."
1616
1633
  ),
1617
1634
  )
1618
1635
  readiness_timeout_s: float = Field(
@@ -1631,6 +1648,24 @@ class LauncherSwapConfig(BaseModel):
1631
1648
  le=600.0,
1632
1649
  description="How often the TTL sweeper background task scans for idle processes.",
1633
1650
  )
1651
+ port_retry_attempts: int = Field(
1652
+ default=2,
1653
+ ge=0,
1654
+ le=5,
1655
+ description=(
1656
+ "[Unreleased] Number of ADDITIONAL spawn attempts (each on a "
1657
+ "freshly picked ephemeral port) after the first, used only "
1658
+ "when a catalog entry's ``port`` is unset and the previous "
1659
+ "attempt fails to become ready. 0 = no retry. Ignored when "
1660
+ "``port`` is set — a fixed port never retries (a second "
1661
+ "attempt would just collide on the same port again). This "
1662
+ "does not close the pick-then-bind TOCTOU window (see "
1663
+ "``coderouter.launcher_swap._pick_ephemeral_port``); it only "
1664
+ "bounds how many times the swap manager re-rolls the dice. "
1665
+ "A fixed ``port`` remains the recommended way to eliminate "
1666
+ "the race entirely (§10 Q2)."
1667
+ ),
1668
+ )
1634
1669
  inject_auto_router_rules: bool = Field(
1635
1670
  default=True,
1636
1671
  description=(
@@ -171,6 +171,13 @@ class ManagedProcess:
171
171
  # under its per-model lock); a launcher auto-restart racing that
172
172
  # re-spawn would fight over the same fixed port.
173
173
  swap_managed: bool = False
174
+ # [Unreleased]: the swap catalog model name (SwapModelSpec.name) this
175
+ # process backs, set by SwapManager._spawn via spawn_process's
176
+ # swap_model kwarg. None for a manually-started process (swap_managed
177
+ # is False) or if a future swap-managed spawn path omits it. Purely
178
+ # informational — surfaced by GET /api/launcher/processes so the
179
+ # /launcher UI can label which catalog entry a running process is.
180
+ swap_model: str | None = None
174
181
  # asyncio subprocess handle — not serialised
175
182
  _proc: Any = field(default=None, repr=False, compare=False)
176
183
 
@@ -1027,7 +1034,13 @@ async def api_launcher_config_debug(request: Request) -> dict[str, Any]:
1027
1034
 
1028
1035
  @router.get("/api/launcher/processes")
1029
1036
  async def api_processes(request: Request) -> dict[str, Any]:
1030
- """List all managed processes."""
1037
+ """List all managed processes.
1038
+
1039
+ [Unreleased]: ``swap_managed`` / ``swap_model`` let the ``/launcher``
1040
+ UI (and other API clients) tell an on-demand SwapManager-spawned
1041
+ process apart from a manually-started one, and label which swap
1042
+ catalog model it backs.
1043
+ """
1031
1044
  reg = _registry(request)
1032
1045
  return {
1033
1046
  "processes": [
@@ -1040,6 +1053,8 @@ async def api_processes(request: Request) -> dict[str, Any]:
1040
1053
  "status": p.status,
1041
1054
  "pid": p.pid,
1042
1055
  "returncode": p.returncode,
1056
+ "swap_managed": p.swap_managed,
1057
+ "swap_model": p.swap_model,
1043
1058
  }
1044
1059
  for p in reg.all()
1045
1060
  ]
@@ -1100,6 +1115,7 @@ async def spawn_process(
1100
1115
  draft_model_path: str | None = None,
1101
1116
  mtp_mode: str = "auto",
1102
1117
  swap_managed: bool = False,
1118
+ swap_model: str | None = None,
1103
1119
  ) -> ManagedProcess:
1104
1120
  """Build argv, spawn the child process, and arm readiness/log tailing.
1105
1121
 
@@ -1116,6 +1132,12 @@ async def spawn_process(
1116
1132
  exist. Any other exception from ``create_subprocess_exec`` propagates
1117
1133
  as-is. Callers decide how to surface these (HTTP 400/500 for
1118
1134
  ``api_start``, a retryable ``AdapterError`` for SwapManager).
1135
+
1136
+ ``swap_model`` ([Unreleased]) is purely informational — the swap
1137
+ catalog model name (``SwapModelSpec.name``), recorded on the
1138
+ resulting ``ManagedProcess`` for ``GET /api/launcher/processes`` /
1139
+ the ``/launcher`` UI. Only ``SwapManager._spawn`` passes it; the
1140
+ manual ``POST /api/launcher/start`` path leaves it ``None``.
1119
1141
  """
1120
1142
  options = options if options is not None else {}
1121
1143
  configured_binary: str | None = None
@@ -1178,6 +1200,7 @@ async def spawn_process(
1178
1200
  spec_auto=spec_auto and fallback_cmd is not None,
1179
1201
  fallback_cmd=fallback_cmd,
1180
1202
  swap_managed=swap_managed,
1203
+ swap_model=swap_model,
1181
1204
  )
1182
1205
  proc.log_tail.append(f"[launcher] cmd: {' '.join(cmd)}")
1183
1206
  for note in spec_notes:
@@ -1378,6 +1401,10 @@ _LAUNCHER_HTML = r"""<!doctype html>
1378
1401
  <style>
1379
1402
  .dot { width:.5rem;height:.5rem;border-radius:9999px;display:inline-block; }
1380
1403
  .tabnum { font-variant-numeric:tabular-nums; }
1404
+ .badge-swap { font-size:10px; text-transform:uppercase; letter-spacing:.02em;
1405
+ background:rgba(99,102,241,.2); color:#a5b4fc; padding:1px 6px;
1406
+ border-radius:9999px; margin-left:6px; vertical-align:middle;
1407
+ white-space:nowrap; }
1381
1408
  .log-box { font-family:monospace;font-size:.75rem;line-height:1.4;
1382
1409
  overflow-y:auto;max-height:14rem;white-space:pre-wrap;word-break:break-all; }
1383
1410
  .model-row:hover { background:rgba(255,255,255,.04);cursor:pointer; }
@@ -1851,7 +1878,17 @@ _LAUNCHER_HTML = r"""<!doctype html>
1851
1878
  try {
1852
1879
  const r = await fetch("/api/launcher/processes");
1853
1880
  const d = await r.json();
1854
- renderProcesses(d.processes || []);
1881
+ const procs = d.processes || [];
1882
+ renderProcesses(procs);
1883
+ // [Unreleased] 404-loop fix: if the process whose logs we're
1884
+ // polling is no longer in the registry (deleted from another
1885
+ // client, swap TTL unload removed it, or the server restarted
1886
+ // under a still-open tab), stop the log polling BEFORE it even
1887
+ // issues a 404 — otherwise poll() would hit
1888
+ // /api/launcher/logs/<stale-id> every POLL_MS forever.
1889
+ if (selectedLogId && !procs.some(p => p.id === selectedLogId)) {
1890
+ markLogGone();
1891
+ }
1855
1892
  } catch (_) {}
1856
1893
  };
1857
1894
 
@@ -1871,8 +1908,14 @@ _LAUNCHER_HTML = r"""<!doctype html>
1871
1908
  ? `<button onclick="deleteProc('${p.id}')" class="btn-sm btn-slate ml-1">✕</button>`
1872
1909
  : "";
1873
1910
  const logBtn = `<button onclick="openLog('${p.id}','${esc(p.name)}')" class="btn-sm btn-indigo ml-1">📋 ログ</button>`;
1911
+ // [Unreleased]: swap badge — marks processes SwapManager spawned
1912
+ // on demand, distinct from manually-started ones. Title shows the
1913
+ // catalog model name when the backend supplied it.
1914
+ const swapBadge = p.swap_managed
1915
+ ? `<span class="badge-swap" title="${esc(p.swap_model ? "swap model: " + p.swap_model : "swap-managed")}">swap</span>`
1916
+ : "";
1874
1917
  return `<tr>
1875
- <td class="py-2 pr-3 font-medium">${esc(p.name)}</td>
1918
+ <td class="py-2 pr-3 font-medium">${esc(p.name)}${swapBadge}</td>
1876
1919
  <td class="py-2 pr-3 text-slate-400">${esc(p.backend)}</td>
1877
1920
  <td class="py-2 pr-3 text-slate-400 truncate max-w-[10rem]" title="${esc(p.model_path)}">${esc(modelName)}</td>
1878
1921
  <td class="py-2 pr-3 text-right">${p.port}</td>
@@ -1912,6 +1955,11 @@ _LAUNCHER_HTML = r"""<!doctype html>
1912
1955
  if (!selectedLogId) return;
1913
1956
  try {
1914
1957
  const r = await fetch(`/api/launcher/logs/${selectedLogId}?n=200`);
1958
+ // [Unreleased] 404-loop fix: the id is gone from the registry —
1959
+ // stop polling instead of retrying (and 404-spamming the serve
1960
+ // log) every POLL_MS forever. The server keeps answering 404 for
1961
+ // unknown ids (correct); the client just has to take the hint.
1962
+ if (r.status === 404) { markLogGone(); return; }
1915
1963
  const d = await r.json();
1916
1964
  const box = document.getElementById("log-box");
1917
1965
  box.textContent = d.logs.join("\n") || "(ログなし)";
@@ -1921,6 +1969,15 @@ _LAUNCHER_HTML = r"""<!doctype html>
1921
1969
  }
1922
1970
  };
1923
1971
 
1972
+ // [Unreleased] 404-loop fix: the process we were tailing no longer
1973
+ // exists server-side. Clear selectedLogId (which is what stops both
1974
+ // poll()'s refreshLogs call and any manual ↻) but keep the panel open
1975
+ // with a terminal message so the user sees WHY the logs stopped.
1976
+ const markLogGone = () => {
1977
+ selectedLogId = null;
1978
+ document.getElementById("log-box").textContent = "(process removed)";
1979
+ };
1980
+
1924
1981
  window.closeLog = () => {
1925
1982
  selectedLogId = null;
1926
1983
  document.getElementById("log-panel").classList.add("hidden");
@@ -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 returning
89
- and the child actually binding the port. Fixed ``port:`` in the
90
- catalog entry avoids the race entirely (§10 Q2, recommended).
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._ttl_seconds is None:
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) < self._ttl_seconds:
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. No-op when TTL is disabled."""
244
- if self._ttl_seconds is None or self._sweeper_task is not None:
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. §10 Q2: one retry on a fresh ephemeral
350
- port when ``spec.port`` is unset and the first attempt doesn't come
351
- up (fixed ports never retry a second attempt would collide again).
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 = 1 if spec.port is not None else 2
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},
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.9.2
3
+ Version: 2.9.3
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
@@ -11,7 +11,7 @@ coderouter/gguf_introspect.py,sha256=KeE00CfbYRa4gSTrzuwC6zVuHi20IzLC4nUQa98BEXI
11
11
  coderouter/hardware.py,sha256=gn3_9qbVcGRR81yKMn1lJE_8-YDRau0LxIH_M-f7pxE,8356
12
12
  coderouter/language_tax.py,sha256=LTbE3tIfoJuV2O3T0NixRKhzq_dEOTUuPEerJv2q9uk,9360
13
13
  coderouter/launcher_speculative.py,sha256=kWaHNmhzBsbWuM_lRGDlbWkgJ1suuxBWQNTKhJOC-yg,12644
14
- coderouter/launcher_swap.py,sha256=Oc9itCKzjSFbE_RWxqcjIJyiYax8o9zZo7czmEI2s-U,19150
14
+ coderouter/launcher_swap.py,sha256=W7qpdB-8Npr8yHGfx0N3SgZ2vDJ6BR9JuxW0I3TaZ-4,23783
15
15
  coderouter/logging.py,sha256=5rfi4_R4aoeeUYDc642dgs61wA0aBX7wvubyB-k3Uyo,58074
16
16
  coderouter/output_filters.py,sha256=0ry_rPiS_kC-FnHgaNVP6v7e6Al2djxzu9vBzZ8kEkE,25314
17
17
  coderouter/token_estimation.py,sha256=iz22vZEEW2P7uKLB2pYvPNpIbZGbgXRO5MtfkS_-9Sk,7531
@@ -25,7 +25,7 @@ coderouter/config/__init__.py,sha256=FODEn74fN-qZnt4INPSHswqhOlEgpL6-_onxsitSx8g
25
25
  coderouter/config/capability_registry.py,sha256=RZgzHF54dXy0CCDocRCz2ee21qXGbbgTSKTkRI9iLL4,16936
26
26
  coderouter/config/env_file.py,sha256=CoMK27fuAXm-NtoLzXb8yN2E-wDFjHQuFwiIlmgTBQw,10356
27
27
  coderouter/config/loader.py,sha256=FUEe8m4Tnmj_aul0vSctD8vKvNW-oLRoMRbTpSKqSmc,4077
28
- coderouter/config/schemas.py,sha256=zMv6UjL2ebcxc3dMIOa9lQjOGvvuHMWCPqoOVA35CEw,117181
28
+ coderouter/config/schemas.py,sha256=FJ0bc23tGbOYA2iQLKmyZN3A8R0vZzSfTY4PT8a5f1s,118933
29
29
  coderouter/data/__init__.py,sha256=uNyfD9jaCvTWsBAWtaw1Fr25OSxzv3psGMfBjT1z0Cc,328
30
30
  coderouter/data/model-capabilities.yaml,sha256=S9jt6SC6-3s2-icZ_n-a14iEMnc2yB1C2R6q-N_tZWQ,19309
31
31
  coderouter/guards/__init__.py,sha256=5qliYBqygvVPneej7nx0uSjxDKsz7t8VzvrDgVBJlvU,1170
@@ -43,7 +43,7 @@ coderouter/ingress/__init__.py,sha256=WQsCH2CGJCAhy0mS6GSEdeYZRkkQu2OHDsP4CJWTLu
43
43
  coderouter/ingress/anthropic_routes.py,sha256=1ThXzx8VgxRV7UCi3NF5ey7kbr0ADPKTXyQuYS5gK1w,27063
44
44
  coderouter/ingress/app.py,sha256=6e_qNlYZIyjLKhY20t0ZNL-x5Fv8R8oaIH_HCdxa9Ic,20921
45
45
  coderouter/ingress/dashboard_routes.py,sha256=1SqNNrVTfQiZcx5n2MvjAzWirittX0e61tCt-MKp4Ag,25617
46
- coderouter/ingress/launcher_routes.py,sha256=kVorBfnWL9Q0a6h-gad2YxgeoZjHFHbL8C4DcHtkigg,82031
46
+ coderouter/ingress/launcher_routes.py,sha256=up9kNAjq1FHgv4_I298u7SkWLcv5T-RLhDcDYbxIp-s,85199
47
47
  coderouter/ingress/metrics_routes.py,sha256=M22dwOGn24P05Ge4W3c7d7mYytSGWjIR-pPSPOAiHJY,3965
48
48
  coderouter/ingress/openai_routes.py,sha256=TrugsQNJfNPgKKLzfT1aXzrs4emWYGE4XebEBuaZoWk,12505
49
49
  coderouter/metrics/__init__.py,sha256=7Es351DPS7yLM0yVF_F0eesmiD83n7Zzhie44chht38,1465
@@ -69,8 +69,8 @@ coderouter/translation/__init__.py,sha256=PYXN7XVEwpG1uC8RLy6fvnGbzEZhhrEuUapH8I
69
69
  coderouter/translation/anthropic.py,sha256=aZkcYH4x82b0x7efJgJb9RWn9Hbyc9pEOthXe4vjUdU,11113
70
70
  coderouter/translation/convert.py,sha256=VV4kpu4dvLmGBmJfQnCLY5ryy_AnQcV2NuwjqRtaR8Q,52633
71
71
  coderouter/translation/tool_repair.py,sha256=DGOKArM53yfVbIfiAveoul1vGq9NEKmjagf43Qh4IkM,57630
72
- coderouter_cli-2.9.2.dist-info/METADATA,sha256=dcDACrTxRsla8y8mcY05IC4KilzZD-co6xkEz9xC_H4,15994
73
- coderouter_cli-2.9.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
74
- coderouter_cli-2.9.2.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
75
- coderouter_cli-2.9.2.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
76
- coderouter_cli-2.9.2.dist-info/RECORD,,
72
+ coderouter_cli-2.9.3.dist-info/METADATA,sha256=ofXxvu6VaPHY-an5OdALTE8qZKIdJvfFK28heY_3ZOM,15994
73
+ coderouter_cli-2.9.3.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
74
+ coderouter_cli-2.9.3.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
75
+ coderouter_cli-2.9.3.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
76
+ coderouter_cli-2.9.3.dist-info/RECORD,,