Skip to content

P1: gateway consumes the meridian-control serving projection - #37

Merged
Lothnic merged 1 commit into
mainfrom
p1-serving-projection
Aug 3, 2026
Merged

P1: gateway consumes the meridian-control serving projection#37
Lothnic merged 1 commit into
mainfrom
p1-serving-projection

Conversation

@Lothnic

@Lothnic Lothnic commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Unblocks managed-engine traffic (DESIGN.md §17, §24 P1).

What

  • meridian-control: each serving-projection endpoint now includes the served model.
  • gateway: new control_plane config block + a background ManagedProjectionSync that polls GET /admin/projection and registers routable managed engines as dynamic backends alongside static config backends.

Safety

  • Static config backends always win on name collisions.
  • A fetch failure keeps the last-known-good managed set — a control-plane blip cannot blackhole live managed backends.
  • Registry swap is atomic within the event loop (single-writer under asyncio).
  • Default off — static-only deployments are unaffected.

Pairs with meridian-node p1-serving-projection (node reports the served model).

Tests

pytest tests/test_managed_sync.py (new: register-alongside-static, deregister-on-removal, keep-on-fetch-failure) + full gateway suite → 540 passed; tests/control → 19 passed; ruff + mypy clean.

🤖 Generated with Claude Code

meridian-control now includes the served model in each serving-projection
endpoint. The gateway gains a control_plane config block and a background
ManagedProjectionSync that polls GET /admin/projection and registers routable
managed engines as dynamic backends alongside static config backends. Static
backends always win on name collisions; a fetch failure keeps the last-known
managed set so a control-plane blip cannot blackhole live backends. Default
off — static-only deployments are unaffected. Unblocks managed-engine traffic
(DESIGN.md 17, 24 P1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 3, 2026 12:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a “managed backends” integration path where the Meridian gateway can poll meridian-control’s serving projection and register routable managed engines as dynamic backends alongside statically configured backends, enabling managed-engine traffic without requiring app-side changes.

Changes:

  • Add control_plane configuration to enable/parameterize polling of meridian-control’s serving projection.
  • Introduce ManagedProjectionSync background poller and extend BackendRegistry to support atomic managed-backend set replacement while preserving static backends.
  • Extend meridian-control’s serving_projection output to include the served model, and add focused tests for registration/deregistration and fetch-failure behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/test_managed_sync.py New unit tests covering managed projection sync behavior (registration, removal, LKG on fetch failure).
meridian/registry/managed.py New poller that fetches /admin/projection and updates the registry’s managed backend set.
meridian/registry/backend.py Registry now preserves a static backend set and supports swapping managed backends in atomically.
meridian/config/models.py Adds ControlPlaneConfig and attaches it to MeridianConfig as control_plane.
meridian/api/state.py Factors backend construction into build_backend() and reuses it for static and managed backends.
meridian/api/main.py Wires ManagedProjectionSync into FastAPI lifespan when control_plane.enabled is set.
meridian_control/service.py Adds model field to each serving projection endpoint payload.
Suppressed comments (2)

tests/test_managed_sync.py:74

  • Close the sync's httpx.AsyncClient after the test to avoid leaking an unclosed client/transport.
async def test_fetch_failure_keeps_last_known_set():
    reg = build_registry(MeridianConfig())
    sync = _sync(reg, [[_ep("e1", "m1")]])  # one good payload, then 503s

    await sync.sync_once()
    await sync.sync_once()  # 503 -> keep previous
    assert reg.get("managed:node_1:e1") is not None

tests/test_managed_sync.py:64

  • Close the sync's httpx.AsyncClient after the test to avoid leaking an unclosed client/transport.
async def test_removed_endpoint_is_deregistered():
    reg = build_registry(MeridianConfig())
    sync = _sync(reg, [[_ep("e1", "m1")], []])  # present, then gone

    await sync.sync_once()
    assert reg.get("managed:node_1:e1") is not None
    await sync.sync_once()
    assert reg.get("managed:node_1:e1") is None

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +63 to +87
async def sync_once(self) -> None:
assert self._client is not None
url = self._config.url.rstrip("/") + "/admin/projection"
try:
resp = await self._client.get(url)
resp.raise_for_status()
endpoints = resp.json().get("endpoints", [])
except (httpx.HTTPError, ValueError) as exc:
logger.warning("Projection fetch failed (%s); keeping last-known managed set", exc)
return
self._registry.set_managed([self._to_backend(e) for e in endpoints if _routable(e)])

def _to_backend(self, e: dict) -> Backend:
bc = BackendConfig(
name=f"managed:{e['node_id']}:{e['engine_id']}",
url=e["endpoint"],
model=e.get("model", ""),
engine="managed",
tags=[self._config.tag],
)
return self._build(bc)


def _routable(e: dict) -> bool:
return bool(e.get("routable")) and bool(e.get("endpoint"))
Comment on lines +58 to +62
async def _loop(self) -> None:
while True:
await asyncio.sleep(self._config.poll_interval_s)
await self.sync_once()

Comment on lines +48 to +56
async def stop(self) -> None:
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
if self._client is not None:
await self._client.aclose()
Comment thread meridian/api/main.py
Comment on lines +148 to +153
# ponytail: bound to the current config; a SIGHUP that changes
# control_plane settings needs a restart to take effect.
managed_sync = ManagedProjectionSync(
state.registry, state.config.control_plane,
build_backend=lambda bc: build_backend(state.config, bc),
)
Comment on lines 335 to 341
if e.get("phase") == "Ready" and e.get("endpoint"):
out.append({
"engine_id": e["engine_id"], "node_id": node.node_id,
"endpoint": e["endpoint"], "provider": "managed",
"endpoint": e["endpoint"], "model": e.get("model", ""),
"provider": "managed",
"routable": bool(lease_valid) and not node.revoked,
})
Comment on lines +42 to +45
static = Backend(BackendConfig(name="static", url="http://s", model="m0"))
reg = build_registry(MeridianConfig(backends=[]))
reg._static = [static]
reg.set_managed([]) # start with just the static backend
Comment on lines +46 to +53
sync = _sync(reg, [[_ep("e1", "m1"), _ep("e2", "m2", routable=False)]])

await sync.sync_once()

names = {b.name for b in reg.all_backends()}
assert names == {"static", "managed:node_1:e1"} # non-routable e2 excluded
assert reg.eligible("m1")[0].name == "managed:node_1:e1"
assert reg.get("static") is static # static untouched

from meridian.api.state import build_backend, build_registry
from meridian.config.models import BackendConfig, ControlPlaneConfig, MeridianConfig
from meridian.registry.backend import Backend
@Lothnic
Lothnic merged commit 6ff2937 into main Aug 3, 2026
6 checks passed
@Lothnic
Lothnic deleted the p1-serving-projection branch August 3, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants