coderouter-cli 2.8.1__py3-none-any.whl → 2.9.1__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/doctor.py CHANGED
@@ -1834,6 +1834,56 @@ async def _probe_cache(
1834
1834
  )
1835
1835
 
1836
1836
 
1837
+ # ---------------------------------------------------------------------------
1838
+ # Phase 2c migration guard (docs/designs/agent-cli-plugin-extraction.md §5.2,
1839
+ # §7 "2c" row): a config-level pre-check, not an HTTP probe. Core removed
1840
+ # its in-core agent_cli adapter in Phase 2c — kind="agent_cli" now resolves
1841
+ # ONLY via the coderouter-plugin-agents adapter plugin
1842
+ # (coderouter/adapters/registry.py's build_adapter). A provider that still
1843
+ # declares kind="agent_cli" without the plugin enabled will fail at engine
1844
+ # startup with a ValueError; surfacing it here lets `coderouter doctor`
1845
+ # catch it ahead of `coderouter serve`, with a copy-paste-able fix.
1846
+ # ---------------------------------------------------------------------------
1847
+
1848
+
1849
+ def _check_agent_cli_plugin_migration(
1850
+ config: CodeRouterConfig, provider: ProviderConfig
1851
+ ) -> ProbeResult | None:
1852
+ """Warn when ``provider`` needs the agent_cli plugin but it isn't enabled.
1853
+
1854
+ Returns ``None`` (no finding) when ``provider.kind`` isn't
1855
+ ``"agent_cli"``, or when ``"agents"`` is already listed in
1856
+ ``plugins.enabled`` — i.e. nothing actionable to report.
1857
+ """
1858
+ if provider.kind != "agent_cli":
1859
+ return None
1860
+ enabled = config.plugins.enabled if config.plugins is not None else []
1861
+ if "agents" in enabled:
1862
+ return None
1863
+ return ProbeResult(
1864
+ name="agent-cli-plugin-migration",
1865
+ verdict=ProbeVerdict.NEEDS_TUNING,
1866
+ detail=(
1867
+ f"provider {provider.name!r} declares kind='agent_cli', but the "
1868
+ "coderouter-plugin-agents adapter plugin is not enabled. As of "
1869
+ "v2.9.0 (Phase 2c), Core no longer ships an in-core agent_cli "
1870
+ "adapter — this provider cannot serve requests until the "
1871
+ "plugin is installed and listed in plugins.enabled."
1872
+ ),
1873
+ target_file="providers.yaml",
1874
+ suggested_patch=(
1875
+ "# install the plugin:\n"
1876
+ 'pip install "coderouter-plugin-agents @ '
1877
+ 'git+https://github.com/zephel01/coderouter-plugin-agents"\n'
1878
+ "\n"
1879
+ "# providers.yaml — enable it (merge into any existing list):\n"
1880
+ "plugins:\n"
1881
+ " enabled:\n"
1882
+ " - agents\n"
1883
+ ),
1884
+ )
1885
+
1886
+
1837
1887
  # ---------------------------------------------------------------------------
1838
1888
  # Orchestration
1839
1889
  # ---------------------------------------------------------------------------
@@ -1872,6 +1922,14 @@ async def check_model(
1872
1922
  resolved_caps=resolved,
1873
1923
  )
1874
1924
 
1925
+ migration = _check_agent_cli_plugin_migration(config, provider)
1926
+ if migration is not None:
1927
+ # Nothing to probe over HTTP — the provider has no adapter until
1928
+ # the plugin is enabled. Report the finding and stop here rather
1929
+ # than sending HTTP probes at a (kind-wise meaningless) base_url.
1930
+ report.results.append(migration)
1931
+ return report
1932
+
1875
1933
  auth_result = await _probe_auth_and_basic_chat(provider)
1876
1934
  report.results.append(auth_result)
1877
1935
 
coderouter/ingress/app.py CHANGED
@@ -305,6 +305,13 @@ def create_app(config_path: str | None = None) -> FastAPI:
305
305
  },
306
306
  )
307
307
 
308
+ # launcher-model-swap.md §6.6 known-trap #9: the TTL sweeper is a
309
+ # background task like continuous-probe above — start it once the
310
+ # event loop is actually running, not at app-construction time.
311
+ swap_manager = getattr(app.state, "swap", None)
312
+ if swap_manager is not None:
313
+ await swap_manager.start()
314
+
308
315
  yield
309
316
 
310
317
  # Graceful shutdown of probe task
@@ -313,6 +320,14 @@ def create_app(config_path: str | None = None) -> FastAPI:
313
320
  with contextlib.suppress(Exception):
314
321
  await probe_task
315
322
 
323
+ # Cancel the swap TTL sweeper *before* shutdown_launcher tears down
324
+ # every ManagedProcess below — shutdown_launcher already stops
325
+ # swap-spawned processes too (they're in the same registry), so
326
+ # this only needs to stop the sweeper loop itself, not race it.
327
+ if swap_manager is not None:
328
+ with contextlib.suppress(Exception):
329
+ await swap_manager.stop()
330
+
316
331
  # Launcher: stop child llama.cpp / vllm processes so they don't orphan.
317
332
  from coderouter.ingress.launcher_routes import shutdown_launcher
318
333
 
@@ -371,6 +386,20 @@ def create_app(config_path: str | None = None) -> FastAPI:
371
386
  app.state.engine = engine
372
387
  app.state.config = config
373
388
 
389
+ # Phase 1 on-demand model swap (docs/designs/launcher-model-swap.md).
390
+ # None (default) leaves the engine's swap dispatch hooks as cheap
391
+ # no-ops — zero behavior change for deployments that don't opt in.
392
+ # Constructed here (needs the real ``app`` for app.state.launcher /
393
+ # app.state.engine) but the TTL sweeper task itself is only started
394
+ # from the lifespan below, once the event loop is actually running.
395
+ swap_cfg = config.launcher.swap if config.launcher is not None else None
396
+ if swap_cfg is not None and swap_cfg.enabled:
397
+ from coderouter.launcher_swap import SwapManager
398
+
399
+ swap_manager = SwapManager(app, swap_cfg, config.launcher)
400
+ app.state.swap = swap_manager
401
+ engine.attach_swap_manager(swap_manager)
402
+
374
403
  # H8: DNS-rebinding protection. Applied to every route so a hostname an
375
404
  # attacker controls cannot be pointed at loopback and used to drive the
376
405
  # local API from a victim's browser. Extra hostnames for deliberate