Skip to content

feat(deploy): migrate stack management to systemd Quadlets#344

Closed
sheepdestroyer wants to merge 5 commits into
masterfrom
feat/quadlet-systemd-consolidated
Closed

feat(deploy): migrate stack management to systemd Quadlets#344
sheepdestroyer wants to merge 5 commits into
masterfrom
feat/quadlet-systemd-consolidated

Conversation

@sheepdestroyer

@sheepdestroyer sheepdestroyer commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Consolidated replacement for #342 and #343

This PR replaces the two closed PRs with one clean review surface. It contains the final subdomain-routing corrections and the full systemd Quadlet migration.

Closes #342
Closes #343

Summary

  • Replace podman play kube stack management with systemd Quadlets: one pod template plus nine container templates, rendered per environment by start-stack.sh.
  • Make systemd the single supervisor: boot enablement through WantedBy=default.target, container recovery through Restart=always, and systemd-aware teardown/redeploy paths.
  • Preserve the existing environment-derived configuration and dev/prod isolation while retaining pod.yaml as the current canonical container-definition reference.
  • Apply the final pure-subdomain routing behavior for LiteLLM, Langfuse, and llama endpoints, including derived dev URL configuration and URL-resolution tests.
  • Add Quadlet template coverage and update deployment documentation.

Important implementation details

  • Templates use the existing _PLACEHOLDER convention and are rendered into ~/.config/containers/systemd/llm-routing/.
  • Containers that require a command override use Entrypoint= (rather than relying on Quadlet Exec= to replace the image entrypoint).
  • Every container receives AddHost=<pod-name>:127.0.0.1, preserving the pod-hostname resolution required by Langfuse.
  • Existing play-kube stacks remain supported as a fallback/migration source; --replace transitions them to the systemd-managed stack.

Prior validation

  • 193/193 unit tests passed during the dev rollout.
  • All nine containers were healthy under their systemd user units.
  • Crash recovery was verified by killing Valkey and observing recovery in under 12 seconds.
  • Canonical dev HTTPS endpoints were verified; transient upstream OpenRouter free-tier 429s were environmental and unrelated.

Review request

Please perform a fresh global review of the complete implementation rather than treating #342/#343 review threads as authoritative. Review the Quadlet lifecycle, migration/teardown error paths, persistent-data ownership, security-sensitive environment handling, and routing behavior across the codebase.

Summary by Sourcery

Migrate stack management from podman play kube to a systemd Quadlet-based Podman stack and align external URL routing with subdomain-based endpoints.

New Features:

  • Introduce systemd Quadlet templates for the LLM routing pod and its nine application containers, rendered per environment from the deployment script.
  • Add support for deriving Litellm and Langfuse public URLs from subdomain-based routing, with optional overrides via environment variables.

Enhancements:

  • Update the deployment script to render and deploy Quadlet units, manage stack lifecycle via systemd user services, and fall back to legacy podman pod control when needed.
  • Refine external URL resolution logic to generate service-specific subdomain URLs while preserving explicit public ports and handling missing hostnames.
  • Improve deployment documentation to describe Quadlet-based management, systemd inspection commands, and the updated container inventory.
  • Extend the production upgrade script to sync the new Quadlet templates alongside existing runtime files.

Tests:

  • Add static tests to validate Quadlet template inventory, pod membership, healthcheck and restart behavior, secret-free templates, and upgrade script coverage.
  • Update router URL resolution tests to reflect the new subdomain-oriented external URL behavior.

boy added 4 commits July 22, 2026 23:46
…lets

Replace 'podman play kube' with declarative Quadlet units rendered from
quadlets/*.pod + quadlets/*.container templates into
~/.config/containers/systemd/llm-routing/. systemd's podman-user-generator
turns them into llm-routing-*.service units, making systemd the single
supervisor: boot auto-start (WantedBy=default.target + linger) and crash
recovery (Restart=always) now come from systemd instead of podman's
restartPolicy.

Key points:
- Templates use the same _PLACEHOLDER convention as pod.yaml; a new
  render_quadlets() in start-stack.sh reuses the existing env-derived
  values (ports, secrets, DATA_ROOT, POD_NAME) so dev/prod parity is
  preserved with plain string replacement (bare scalars, not YAML).
- deploy_fresh_pod() now: render configs -> render quadlets ->
  daemon-reload -> start/restart llm-routing-pod.service. The bare
  'restart existing' path and safe_pod_teardown() detect systemd-managed
  stacks and route through systemctl accordingly.
- Entrypoint semantics: Quadlet Exec= only sets args (appended to the
  image entrypoint), so containers where pod.yaml used command: now set
  Entrypoint= explicitly (litellm venv python, valkey-server,
  redis-server, /bin/sh for the router). MinIO keeps its image
  entrypoint with Exec= providing the server args, matching pod.yaml
  args: behavior.
- AddHost=<POD_NAME>:127.0.0.1 on all containers replicates the
  /etc/hosts entry play kube injected, required by langfuse 3.222+
  which resolves the pod hostname at startup.
- Healthchecks ported from livenessProbe exec commands (HealthCmd,
  HealthStartPeriod maps initialDelaySeconds).

Verified on dev: all 9 containers healthy, 193/193 unit tests pass,
crash recovery (kill -> systemd auto-restart <12s), canonical HTTPS
endpoints green (model timeouts on free tier are pre-existing,
identical on prod play-kube).
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@sourcery-ai

sourcery-ai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Migrates stack management from podman play kube to a systemd Quadlet-based stack while switching external routing to pure subdomain-style URLs, adding Quadlet rendering/installation logic, tests, and updated deployment docs and upgrade flow.

Flow diagram for Quadlet-based stack deployment via start-stack.sh

flowchart TD
  A[start-stack.sh --replace or --full-rebuild] --> B[deploy_fresh_pod]
  B --> C[generate_clickhouse_config]
  B --> D[render_litellm_config]
  B --> E[render_router_config]
  B --> F[deploy_quadlets]
  F --> G[render_quadlets]
  G --> H[Write Quadlet .pod and .container files to QUADLET_DIR]
  H --> I[systemctl --user daemon-reload]
  I --> J{llm-routing-pod.service active?}
  J -->|yes| K[systemctl --user restart llm-routing-pod.service]
  J -->|no| L[systemctl --user start llm-routing-pod.service]
  K --> M[setup_minio_buckets]
  L --> M
  M --> N[verify_stack_health]

  subgraph Teardown_or_restart
    O[start-stack.sh] --> P{pod exists?}
    P -->|yes and --replace| Q[safe_pod_teardown]
    Q --> R[Stop llm-routing-pod.service if active]
    R --> S[podman pod rm -f POD_NAME]
    S --> T[cleanup_zombie_ports]
    T --> B
    P -->|no or no --replace| U{llm-routing-pod.service active?}
    U -->|yes| V[systemctl --user restart llm-routing-pod.service]
    U -->|no| W[podman pod restart POD_NAME]
  end
Loading

File-Level Changes

Change Details Files
Switch stack lifecycle from podman play kube to systemd Quadlets with environment-based template rendering and systemd-aware start/stop paths.
  • Add Quadlet rendering pipeline that expands _PLACEHOLDER-based .pod and .container templates into ~/.config/containers/systemd/llm-routing with owner-only permissions.
  • Introduce deploy_quadlets() to render templates, run systemctl --user daemon-reload, and start or restart llm-routing-pod.service as the primary deployment mechanism.
  • Update deploy_fresh_pod() and the default start-stack.sh path to use systemd-managed Quadlets instead of piping pod.yaml into podman play kube.
  • Modify safe_pod_teardown() and non-replace restart paths to stop/restart the stack via systemd when llm-routing-pod.service is active, falling back to podman pod operations otherwise.
  • Ensure quadlet rendering mirrors pod.yaml URL derivation (including PROXY_BASE_URL/NEXTAUTH_URL) and enforces no unresolved placeholders.
start-stack.sh
Adopt pure subdomain-based routing for Langfuse, LiteLLM, and llama endpoints and improve URL resolution behind proxies and non-standard ports.
  • Change PUBLIC_BASE_URL-derived PROXY_BASE_URL and NEXTAUTH_URL to default to litellm. and langfuse. subdomains, with overrides via explicit environment variables.
  • Update resolve_external_urls to emit langfuse./litellm./llama. subdomain URLs, stripping dashboard/service prefixes and preserving explicit public ports from the chosen netloc.
  • Handle invalid public ports defensively in resolve_external_urls and log a warning when parsing fails.
  • Adjust existing router URL-resolution tests to assert the new subdomain-style URLs and updated port-handling semantics.
start-stack.sh
router/main.py
router/tests/test_resolve_external_urls.py
Introduce and validate systemd Quadlet templates as the canonical runtime definition for the stack.
  • Add Quadlet pod and container templates for all nine application containers, using Pod=llm-routing.pod membership and ContainerName=POD_NAME_PLACEHOLDER-* naming.
  • Ensure containers express health checks with HealthCmd, HealthOnFailure=kill, and Restart=always for systemd-driven recovery.
  • Guarantee all templates rely on _PLACEHOLDER substitution (no baked-in secrets) and include AddHost-based intra-pod host mapping consistent with prior behavior (via placeholders).
  • Add static tests to assert template inventory (9 containers + pod), health/restart semantics, placeholder usage, and that upgrade-prod.sh syncs quadlets and start-stack renders secure owner-only files.
quadlets/llm-routing.pod
quadlets/llm-routing-clickhouse.container
quadlets/llm-routing-langfuse-web.container
quadlets/llm-routing-langfuse-worker.container
quadlets/llm-routing-litellm.container
quadlets/llm-routing-minio.container
quadlets/llm-routing-postgres.container
quadlets/llm-routing-router.container
quadlets/llm-routing-valkey-cache.container
quadlets/llm-routing-valkey-lf.container
tests/test_quadlet_templates.py
start-stack.sh
scripts/upgrade-prod.sh
Update operational and upgrade documentation to reflect systemd Quadlet-based management and container inventory.
  • Revise README startup instructions to describe Quadlet-based lifecycle, new semantics for ./start-stack.sh and flags, and how to inspect systemd units and logs.
  • Clarify that prod-router-pod has nine application containers plus Podman’s infra container.
  • Update scripts/README.md to describe start-stack.sh as a Quadlet orchestrator and document the quadlets/ directory and systemd/journalctl diagnostics.
  • Extend upgrade-prod.sh to treat quadlets/ as a first-class runtime artifact: presence checks, dry-run diffing, overwrite warning, and rsync with --delete before other directories.
README.md
scripts/README.md
scripts/upgrade-prod.sh

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions github-actions Bot added documentation Improvements or additions to documentation router scripts tests labels Jul 22, 2026
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@sheepdestroyer, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c22fe508-98f1-4878-b263-b153b1bc021d

📥 Commits

Reviewing files that changed from the base of the PR and between 7d42cc4 and f8a4a53.

📒 Files selected for processing (18)
  • .env.dev
  • README.md
  • quadlets/llm-routing-clickhouse.container
  • quadlets/llm-routing-langfuse-web.container
  • quadlets/llm-routing-langfuse-worker.container
  • quadlets/llm-routing-litellm.container
  • quadlets/llm-routing-minio.container
  • quadlets/llm-routing-postgres.container
  • quadlets/llm-routing-router.container
  • quadlets/llm-routing-valkey-cache.container
  • quadlets/llm-routing-valkey-lf.container
  • quadlets/llm-routing.pod
  • router/main.py
  • router/tests/test_resolve_external_urls.py
  • scripts/README.md
  • scripts/upgrade-prod.sh
  • start-stack.sh
  • tests/test_quadlet_templates.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/quadlet-systemd-consolidated

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 2 issues, and left some high level feedback:

  • The PROXY_BASE_URL/NEXTAUTH_URL derivation logic is now duplicated between the pod YAML rendering and the Quadlet renderer; consider factoring this into a single helper or script segment to avoid drift between the two paths.
  • In resolve_external_urls, the regex-based stripping of subdomain prefixes (dashboard, litellm, langfuse, llama) could leave host_base empty or mis-handle unexpected hostnames; it may be safer to explicitly validate/normalize the host and fall back to the routing domain when stripping would produce an invalid base.
  • deploy_quadlets exits hard if systemctl --user daemon-reload fails (e.g., no user systemd), whereas safe_pod_teardown supports both systemd-managed and plain pod stacks; consider aligning error handling so non-systemd environments degrade gracefully or are detected earlier with a clear guard.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The PROXY_BASE_URL/NEXTAUTH_URL derivation logic is now duplicated between the pod YAML rendering and the Quadlet renderer; consider factoring this into a single helper or script segment to avoid drift between the two paths.
- In resolve_external_urls, the regex-based stripping of subdomain prefixes (dashboard, litellm, langfuse, llama) could leave host_base empty or mis-handle unexpected hostnames; it may be safer to explicitly validate/normalize the host and fall back to the routing domain when stripping would produce an invalid base.
- deploy_quadlets exits hard if systemctl --user daemon-reload fails (e.g., no user systemd), whereas safe_pod_teardown supports both systemd-managed and plain pod stacks; consider aligning error handling so non-systemd environments degrade gracefully or are detected earlier with a clear guard.

## Individual Comments

### Comment 1
<location path="start-stack.sh" line_range="713-715" />
<code_context>
+# Derive PROXY_BASE_URL and NEXTAUTH_URL (favoring explicit env or subdomain formatting)
 public_base_url = os.environ["PUBLIC_BASE_URL"].rstrip("/")
-proxy_base_url = f"{public_base_url}/litellm"
+parsed_pub = urllib.parse.urlparse(public_base_url)
+scheme = parsed_pub.scheme or "https"
+host = parsed_pub.netloc or parsed_pub.path.split("/")[0] or "vendeuvre.lan"
+
+if "PROXY_BASE_URL" in os.environ:
</code_context>
<issue_to_address>
**issue (bug_risk):** Align PUBLIC_BASE_URL host fallback between pod.yaml rendering and quadlet rendering to avoid inconsistent URLs.

In pod.yaml we fall back to the hard-coded host "vendeuvre.lan", but in the quadlet renderer we fall back to `os.environ["ROUTING_DOMAIN"]`. This can produce different `PROXY_BASE_URL` / `NEXTAUTH_URL` values when `PUBLIC_BASE_URL` is missing/invalid or when `ROUTING_DOMAIN` differs from the baked-in default, and contradicts the claim that quadlets derive URLs exactly like `render_pod_yaml`.

Please centralize the `scheme`/`host` derivation in a shared helper so both paths use the same fallback logic, and replace the magic literal "vendeuvre.lan" with a configurable default (e.g. `ROUTING_DOMAIN` or a dedicated env var).
</issue_to_address>

### Comment 2
<location path="router/tests/test_resolve_external_urls.py" line_range="67-71" />
<code_context>
-    assert lf == "http://[::1]:8000/llm-routing/langfuse"
-    assert ll == "http://[::1]:8000/llm-routing/litellm/ui"
-    assert lm == "http://[::1]:8000/llm-routing/llama/"
+    # Valid request host is converted to service subdomains while preserving
+    # its explicit port, rather than leaking the configured IPv6 fallback.
+    assert lf == "http://langfuse.sub.vendeuvre.lan:8000"
+    assert ll == "http://litellm.sub.vendeuvre.lan:8000/ui/"
+    assert lm == "http://llama.sub.vendeuvre.lan:8000/"


</code_context>
<issue_to_address>
**suggestion (testing):** Cover the case where the base URL host is valid but has no explicit port

Right now we only cover the explicit port case. Please add a test where the base hostname is valid but omits the port (e.g. `http://sub.vendeuvre.lan/`), so `parsed_public.port` is `None`. That test should verify we still generate the three service subdomains, don’t append an empty `:`, and keep `/ui/` and `/` paths consistent. This will exercise the other branch of the `port_suffix` logic.

Suggested implementation:

```python
def test_resolve_with_valid_base_request_without_port(req_without_port):
    lf, ll, lm = resolve_external_urls(req_without_port)

    # When the base URL host is valid but omits the port (e.g. http://sub.vendeuvre.lan/),
    # we should still generate service-specific subdomains, not append an empty port,
    # and keep `/ui/` and `/` paths consistent.
    assert lf == "http://langfuse.sub.vendeuvre.lan"
    assert ll == "http://litellm.sub.vendeuvre.lan/ui/"
    assert lm == "http://llama.sub.vendeuvre.lan/"

```

To make this test pass you also need to:
1. Add a `req_without_port` fixture in this test module (or a shared `conftest.py`) that builds a request whose public/base URL is `http://sub.vendeuvre.lan/` (or equivalent), such that `parsed_public.port` is `None` inside `resolve_external_urls`.
2. Ensure the fixture mirrors the existing request construction used in the explicit-port test (same path/prefix, headers, scheme, etc.) but simply omits the `:port` in the host, so this test exercises the `port_suffix` branch where no port is appended.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread start-stack.sh Outdated
Comment on lines +713 to +715
parsed_pub = urllib.parse.urlparse(public_base_url)
scheme = parsed_pub.scheme or "https"
host = parsed_pub.netloc or parsed_pub.path.split("/")[0] or "vendeuvre.lan"

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.

issue (bug_risk): Align PUBLIC_BASE_URL host fallback between pod.yaml rendering and quadlet rendering to avoid inconsistent URLs.

In pod.yaml we fall back to the hard-coded host "vendeuvre.lan", but in the quadlet renderer we fall back to os.environ["ROUTING_DOMAIN"]. This can produce different PROXY_BASE_URL / NEXTAUTH_URL values when PUBLIC_BASE_URL is missing/invalid or when ROUTING_DOMAIN differs from the baked-in default, and contradicts the claim that quadlets derive URLs exactly like render_pod_yaml.

Please centralize the scheme/host derivation in a shared helper so both paths use the same fallback logic, and replace the magic literal "vendeuvre.lan" with a configurable default (e.g. ROUTING_DOMAIN or a dedicated env var).

Comment on lines +67 to +71
# Valid request host is converted to service subdomains while preserving
# its explicit port, rather than leaking the configured IPv6 fallback.
assert lf == "http://langfuse.sub.vendeuvre.lan:8000"
assert ll == "http://litellm.sub.vendeuvre.lan:8000/ui/"
assert lm == "http://llama.sub.vendeuvre.lan:8000/"

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.

suggestion (testing): Cover the case where the base URL host is valid but has no explicit port

Right now we only cover the explicit port case. Please add a test where the base hostname is valid but omits the port (e.g. http://sub.vendeuvre.lan/), so parsed_public.port is None. That test should verify we still generate the three service subdomains, don’t append an empty :, and keep /ui/ and / paths consistent. This will exercise the other branch of the port_suffix logic.

Suggested implementation:

def test_resolve_with_valid_base_request_without_port(req_without_port):
    lf, ll, lm = resolve_external_urls(req_without_port)

    # When the base URL host is valid but omits the port (e.g. http://sub.vendeuvre.lan/),
    # we should still generate service-specific subdomains, not append an empty port,
    # and keep `/ui/` and `/` paths consistent.
    assert lf == "http://langfuse.sub.vendeuvre.lan"
    assert ll == "http://litellm.sub.vendeuvre.lan/ui/"
    assert lm == "http://llama.sub.vendeuvre.lan/"

To make this test pass you also need to:

  1. Add a req_without_port fixture in this test module (or a shared conftest.py) that builds a request whose public/base URL is http://sub.vendeuvre.lan/ (or equivalent), such that parsed_public.port is None inside resolve_external_urls.
  2. Ensure the fixture mirrors the existing request construction used in the explicit-port test (same path/prefix, headers, scheme, etc.) but simply omits the :port in the host, so this test exercises the port_suffix branch where no port is appended.

@sheepdestroyer

Copy link
Copy Markdown
Owner Author

Review: changes requested

I performed a fresh full review of the current master...HEAD implementation, including all Quadlet templates, lifecycle/rendering code, routing changes, migration paths, generated user-unit behavior, and live dev-stack verification.

Blocking findings

  1. Quadlet rendering corrupts values with spaces

    • quadlets/llm-routing-langfuse-web.container:36,38
    • The raw Environment= substitution does not escape systemd values. On the live generated dev unit, Local Development became LANGFUSE_INIT_ORG_NAME=Local plus a separate --env Development; Triage Gateway similarly became Triage plus Gateway.
    • Please render safely quoted/escaped unit values, or preferably generate a 0600 EnvironmentFile and reference it. Add tests using whitespace and systemd-special characters in rendered values/secrets.
  2. The canonical verifier and docs retain old path-based service URLs

    • scripts/verification/verify_canonical_endpoints.py:600-607
    • README.md:847-855
    • The implementation now uses service subdomains, but the verifier still tests PUBLIC_BASE_URL + /litellm/ui/ and + /langfuse.
    • Live --dev verification confirmed both return 404:
      • https://dev.vendeuvre.lan/litellm/ui/
      • https://dev.vendeuvre.lan/langfuse
    • Update the verifier and docs to test/document https://litellm.<base>/ui/, https://langfuse.<base>/, and https://llama.<base>/, preferably deriving them from shared URL-resolution logic.
  3. Migration ownership detection needs an explicit state model

    • start-stack.sh:536-546,894-951
    • Management mode is inferred primarily from whether llm-routing-pod.service is currently active. Interrupted/failed/stopped Quadlet units and legacy play-kube pods need explicit reconciliation before --replace; otherwise unit state and Podman state can diverge.
    • Please determine ownership from generated-unit configuration / PODMAN_SYSTEMD_UNIT metadata and handle active, inactive, failed, absent, and legacy states explicitly. Add migration/retry coverage.

Validation performed

  • Full test suite: 201 passed, 1 warning
  • Focused changed-area tests: 9 passed
  • Live generated Quadlet stack: all 10 generated user units active; all 9 application containers healthy.
  • Entrypoint/Exec behavior was checked against image metadata and generated Podman commands; LiteLLM, router, Valkey, and MinIO semantics match the legacy pod intent.
  • Canonical dev verification: 24/28 passed. The two stale public service paths above failed. Two auto-routing calls timed out while direct requests succeeded; I did not establish that as a PR regression.

The Quadlet architecture, health/restart wiring, container command overrides, persistent mounts, template secrecy, and strict routing-domain validation otherwise look sound.

@sheepdestroyer

Copy link
Copy Markdown
Owner Author

Closing in favor of a fresh consolidated PR after addressing every valid review finding. The replacement PR includes safe Quadlet environment quoting, shared URL derivation, explicit Quadlet ownership reconciliation, subdomain-aware canonical verification/docs, and regression coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation router scripts tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant