Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# One line ending everywhere, so generated files compare equal on any platform.
* text=auto eol=lf
*.png binary
9 changes: 9 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,17 @@ jobs:
- uses: actions/checkout@v7
with:
ref: ${{ env.RELEASE_REF }}
fetch-depth: 0
- name: Describe the version being packaged
# The tag for this commit is cut after CD starts, so this is the last
# release plus the distance from it, which is what the commit is.
id: version
run: echo "version=$(git describe --tags --always | sed 's/^v//')" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
with:
context: .
build-args: VERSION=${{ steps.version.outputs.version }}
outputs: type=oci,dest=${{ runner.temp }}/local-llm-router.tar
tags: local-llm-router:${{ env.RELEASE_REF }}
- uses: actions/upload-artifact@v7
Expand Down Expand Up @@ -65,6 +72,8 @@ jobs:
- name: Verify the Helm chart matches the manifests
run: python -m llm_router.chart --check && helm lint deploy/helm/llm-routing
- name: Package the Helm chart
# The released chart, versioned by its tag, is attached to the GitHub
# release; this one is the unversioned build for inspection.
run: helm package deploy/helm/llm-routing --destination chart
- name: Verify every catalog adapter has a matching recipe
run: python -m llm_router.adapters check
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,14 @@ jobs:
--title "$VERSION" \
--target "$RELEASE_SHA" \
--generate-notes

- name: Attach the Helm chart for this version
if: steps.version.outputs.bump != 'none'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
# The chart in the repository carries no version; the tag is the version.
helm package deploy/helm/llm-routing \
--version "${VERSION#v}" --app-version "${VERSION#v}" --destination dist
gh release upload "$VERSION" dist/*.tgz --repo "${{ github.repository }}" --clobber
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
FROM python:3.13-slim AS builder

WORKDIR /build
# The build context carries no git history, so the release version is passed
# in; without it the image reports 0.0.0 rather than guessing.
ARG VERSION=0.0.0
ENV SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION}
COPY pyproject.toml README.md ./
COPY src ./src
RUN python -m pip wheel --no-cache-dir --wheel-dir /wheels .
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ helm upgrade --install llm-routing deploy/helm/llm-routing \
| `gateway.replicas` | `2` | Starting gateway size. |
| `gateway.autoscaling.minReplicas` / `maxReplicas` | `2` / `20` | KEDA bounds. |

Each GitHub release carries the chart packaged at that version. The chart in the repository
has no version of its own: like the Python package, it takes the release tag.

The chart is generated from the manifests and never edited by hand; tests check that it renders
exactly what kustomize renders, in both modes.

Expand Down
6 changes: 4 additions & 2 deletions deploy/helm/llm-routing/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ apiVersion: v2
name: llm-routing
description: OpenAI-compatible gateway, policy router and local LLM serving plane.
type: application
version: 0.1.0
appVersion: "0.1.0"
# Set from the release tag when the chart is packaged:
# helm package --version <tag> --app-version <tag>
version: 0.0.0
appVersion: "0.0.0"
kubeVersion: ">=1.27.0-0"
14 changes: 12 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[build-system]
requires = ["hatchling"]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[project]
name = "local-llm-router"
version = "0.1.0"
dynamic = ["version"]
description = "OpenAI-compatible gateway and policy router for local LLM inference."
readme = "README.md"
requires-python = ">=3.11"
Expand Down Expand Up @@ -57,6 +57,16 @@ dev = [
"types-PyYAML>=6.0.12.20260906,<7",
]

# The version is the release tag. Releases are tagged by the release workflow
# after a merge, so no file in the repository has to be edited to cut one.
[tool.hatch.version]
source = "vcs"

[tool.hatch.version.raw-options]
local_scheme = "no-local-version"
# Used when there is no git history to read, as in a source archive.
fallback_version = "0.0.0"

[tool.hatch.build.targets.wheel]
packages = ["src/llm_router"]

Expand Down
7 changes: 6 additions & 1 deletion src/llm_router/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
"""Local LLM inference gateway and routing policy package."""

__version__ = "0.1.0"
from importlib.metadata import PackageNotFoundError, version

try:
__version__ = version("local-llm-router")
except PackageNotFoundError: # pragma: no cover - running from an uninstalled tree
__version__ = "0.0.0"
3 changes: 2 additions & 1 deletion src/llm_router/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from fastapi.responses import JSONResponse, StreamingResponse
from opentelemetry.trace import TracerProvider

from llm_router import __version__
from llm_router.admission import (
AdmissionController,
AdmissionRejectedError,
Expand Down Expand Up @@ -259,7 +260,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:

app = FastAPI(
title="Local LLM Inference Router",
version="0.1.0",
version=__version__,
lifespan=lifespan,
)

Expand Down
25 changes: 11 additions & 14 deletions src/llm_router/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"""

import re
import tomllib
from collections.abc import Sequence
from pathlib import Path
from typing import Any
Expand Down Expand Up @@ -178,25 +177,23 @@ def _values(source_root: Path) -> str:
return "\n".join(lines) + "\n"


def _chart_metadata(source_root: Path) -> str:
project = tomllib.loads((source_root / "pyproject.toml").read_text(encoding="utf-8"))
version = project["project"]["version"]
return (
"apiVersion: v2\n"
f"name: {CHART_NAME}\n"
"description: OpenAI-compatible gateway, policy router and local LLM serving plane.\n"
"type: application\n"
f"version: {version}\n"
f'appVersion: "{version}"\n'
'kubeVersion: ">=1.27.0-0"\n'
)
CHART_METADATA = f"""apiVersion: v2
name: {CHART_NAME}
description: OpenAI-compatible gateway, policy router and local LLM serving plane.
type: application
# Set from the release tag when the chart is packaged:
# helm package --version <tag> --app-version <tag>
version: 0.0.0
appVersion: "0.0.0"
kubeVersion: ">=1.27.0-0"
"""


def build_chart(source_root: Path = Path(".")) -> dict[str, str]:
"""Every file of the chart, keyed by its path inside the chart directory."""

files = {
"Chart.yaml": _chart_metadata(source_root),
"Chart.yaml": CHART_METADATA,
"values.yaml": _values(source_root),
"templates/_helpers.tpl": HELPERS,
"templates/dashboards.yaml": DASHBOARDS,
Expand Down
5 changes: 5 additions & 0 deletions tests/unit/test_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ def resources(rendered: str) -> dict[Key, dict[str, Any]]:
# Kustomize moves a patched variable to the front; order means nothing.
for container in document["spec"]["template"]["spec"]["containers"]:
container.get("env", []).sort(key=lambda item: item["name"])
if document["kind"] == "ConfigMap":
# A Windows checkout may hold the same file with different line endings.
document["data"] = {
name: content.replace("\r\n", "\n") for name, content in document["data"].items()
}
found[document["kind"], document["metadata"]["name"]] = document
return found

Expand Down
Loading