lmrelay 0.0.0__tar.gz
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.
- lmrelay-0.0.0/.dockerignore +32 -0
- lmrelay-0.0.0/.env.example +60 -0
- lmrelay-0.0.0/.github/workflows/ci.yml +31 -0
- lmrelay-0.0.0/.github/workflows/publish.yml +23 -0
- lmrelay-0.0.0/.github/workflows/release.yml +42 -0
- lmrelay-0.0.0/.gitignore +36 -0
- lmrelay-0.0.0/CHANGELOG.md +146 -0
- lmrelay-0.0.0/Dockerfile +16 -0
- lmrelay-0.0.0/LICENSE +21 -0
- lmrelay-0.0.0/Makefile +83 -0
- lmrelay-0.0.0/PKG-INFO +316 -0
- lmrelay-0.0.0/README.md +273 -0
- lmrelay-0.0.0/deploy/README.md +64 -0
- lmrelay-0.0.0/deploy/systemd/lmrelay-watcher.service +29 -0
- lmrelay-0.0.0/deploy/systemd/lmrelay.service +42 -0
- lmrelay-0.0.0/docker-compose.yml +79 -0
- lmrelay-0.0.0/docs/adr/0001-canonical-form-as-routing-pivot.md +51 -0
- lmrelay-0.0.0/docs/agents.md +30 -0
- lmrelay-0.0.0/docs/api/anthropic.md +39 -0
- lmrelay-0.0.0/docs/api/ollama.md +33 -0
- lmrelay-0.0.0/docs/api/openai.md +27 -0
- lmrelay-0.0.0/docs/architecture.md +54 -0
- lmrelay-0.0.0/docs/providers.md +47 -0
- lmrelay-0.0.0/docs/security.md +50 -0
- lmrelay-0.0.0/lmrelay/__init__.py +5 -0
- lmrelay-0.0.0/lmrelay/__main__.py +8 -0
- lmrelay-0.0.0/lmrelay/cli/__init__.py +3 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_audit.py +96 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_bench.py +89 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_bind.py +28 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_dashboard.py +18 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_doctor.py +110 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_init.py +70 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_keys.py +66 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_list.py +93 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_migrate_ollama.py +78 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_ping.py +244 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_reload.py +61 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_run.py +30 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_serve.py +34 -0
- lmrelay-0.0.0/lmrelay/cli/cmd_telemetry.py +34 -0
- lmrelay-0.0.0/lmrelay/cli/main.py +67 -0
- lmrelay-0.0.0/lmrelay/cli/watcher.py +117 -0
- lmrelay-0.0.0/lmrelay/config.py +128 -0
- lmrelay-0.0.0/lmrelay/core/__init__.py +3 -0
- lmrelay-0.0.0/lmrelay/core/aliases.py +128 -0
- lmrelay-0.0.0/lmrelay/core/auth.py +18 -0
- lmrelay-0.0.0/lmrelay/core/catalog_index.py +104 -0
- lmrelay-0.0.0/lmrelay/core/cooldown.py +58 -0
- lmrelay-0.0.0/lmrelay/core/errors.py +27 -0
- lmrelay-0.0.0/lmrelay/core/events.py +53 -0
- lmrelay-0.0.0/lmrelay/core/health.py +61 -0
- lmrelay-0.0.0/lmrelay/core/keys.py +205 -0
- lmrelay-0.0.0/lmrelay/core/model_catalog.py +57 -0
- lmrelay-0.0.0/lmrelay/core/model_resolver.py +66 -0
- lmrelay-0.0.0/lmrelay/core/paths.py +37 -0
- lmrelay-0.0.0/lmrelay/core/provider_filter.py +59 -0
- lmrelay-0.0.0/lmrelay/core/router.py +181 -0
- lmrelay-0.0.0/lmrelay/core/telemetry.py +89 -0
- lmrelay-0.0.0/lmrelay/core/toml_config.py +66 -0
- lmrelay-0.0.0/lmrelay/core/types.py +65 -0
- lmrelay-0.0.0/lmrelay/integrations/__init__.py +3 -0
- lmrelay-0.0.0/lmrelay/integrations/aider.py +33 -0
- lmrelay-0.0.0/lmrelay/integrations/base.py +27 -0
- lmrelay-0.0.0/lmrelay/integrations/claude_code.py +42 -0
- lmrelay-0.0.0/lmrelay/integrations/codex.py +44 -0
- lmrelay-0.0.0/lmrelay/integrations/continue_.py +44 -0
- lmrelay-0.0.0/lmrelay/integrations/cursor.py +31 -0
- lmrelay-0.0.0/lmrelay/integrations/gemini.py +43 -0
- lmrelay-0.0.0/lmrelay/integrations/lobechat.py +35 -0
- lmrelay-0.0.0/lmrelay/integrations/openclaw.py +54 -0
- lmrelay-0.0.0/lmrelay/integrations/registry.py +25 -0
- lmrelay-0.0.0/lmrelay/logging_setup.py +24 -0
- lmrelay-0.0.0/lmrelay/providers/__init__.py +3 -0
- lmrelay-0.0.0/lmrelay/providers/base.py +67 -0
- lmrelay-0.0.0/lmrelay/providers/cerebras.py +64 -0
- lmrelay-0.0.0/lmrelay/providers/cloudflare_wai.py +106 -0
- lmrelay-0.0.0/lmrelay/providers/gemini.py +144 -0
- lmrelay-0.0.0/lmrelay/providers/groq.py +71 -0
- lmrelay-0.0.0/lmrelay/providers/huggingface.py +80 -0
- lmrelay-0.0.0/lmrelay/providers/nvidia_nim.py +80 -0
- lmrelay-0.0.0/lmrelay/providers/ollama_backend.py +173 -0
- lmrelay-0.0.0/lmrelay/providers/openai_compat.py +189 -0
- lmrelay-0.0.0/lmrelay/providers/openrouter.py +118 -0
- lmrelay-0.0.0/lmrelay/providers/registry.py +78 -0
- lmrelay-0.0.0/lmrelay/server/__init__.py +3 -0
- lmrelay-0.0.0/lmrelay/server/app.py +139 -0
- lmrelay-0.0.0/lmrelay/server/errors.py +60 -0
- lmrelay-0.0.0/lmrelay/server/middleware.py +45 -0
- lmrelay-0.0.0/lmrelay/server/routes_admin.py +62 -0
- lmrelay-0.0.0/lmrelay/server/routes_anthropic.py +36 -0
- lmrelay-0.0.0/lmrelay/server/routes_health.py +49 -0
- lmrelay-0.0.0/lmrelay/server/routes_ollama.py +224 -0
- lmrelay-0.0.0/lmrelay/server/routes_openai.py +129 -0
- lmrelay-0.0.0/lmrelay/translators/__init__.py +3 -0
- lmrelay-0.0.0/lmrelay/translators/anthropic_to_canonical.py +117 -0
- lmrelay-0.0.0/lmrelay/translators/canonical_to_anthropic.py +166 -0
- lmrelay-0.0.0/lmrelay/translators/canonical_to_ollama.py +124 -0
- lmrelay-0.0.0/lmrelay/translators/canonical_to_openai.py +178 -0
- lmrelay-0.0.0/lmrelay/translators/ollama_to_canonical.py +79 -0
- lmrelay-0.0.0/lmrelay/translators/openai_to_canonical.py +63 -0
- lmrelay-0.0.0/lmrelay/translators/stream_framing.py +37 -0
- lmrelay-0.0.0/lmrelay/tui/__init__.py +3 -0
- lmrelay-0.0.0/lmrelay/tui/dashboard.py +104 -0
- lmrelay-0.0.0/lmrelay.toml.example +106 -0
- lmrelay-0.0.0/pyproject.toml +100 -0
- lmrelay-0.0.0/tests/__init__.py +0 -0
- lmrelay-0.0.0/tests/conftest.py +30 -0
- lmrelay-0.0.0/tests/integration/__init__.py +0 -0
- lmrelay-0.0.0/tests/integration/conftest.py +59 -0
- lmrelay-0.0.0/tests/integration/test_admin_reload.py +102 -0
- lmrelay-0.0.0/tests/integration/test_api_anthropic.py +42 -0
- lmrelay-0.0.0/tests/integration/test_api_ollama.py +97 -0
- lmrelay-0.0.0/tests/integration/test_api_openai.py +72 -0
- lmrelay-0.0.0/tests/integration/test_auth.py +37 -0
- lmrelay-0.0.0/tests/integration/test_disabled_providers.py +26 -0
- lmrelay-0.0.0/tests/integration/test_streaming.py +52 -0
- lmrelay-0.0.0/tests/unit/__init__.py +0 -0
- lmrelay-0.0.0/tests/unit/test_aliases.py +48 -0
- lmrelay-0.0.0/tests/unit/test_config_disabled.py +37 -0
- lmrelay-0.0.0/tests/unit/test_cooldown.py +55 -0
- lmrelay-0.0.0/tests/unit/test_gemini_provider.py +143 -0
- lmrelay-0.0.0/tests/unit/test_health.py +40 -0
- lmrelay-0.0.0/tests/unit/test_key_filter.py +140 -0
- lmrelay-0.0.0/tests/unit/test_keys_parsing.py +62 -0
- lmrelay-0.0.0/tests/unit/test_keys_toml.py +86 -0
- lmrelay-0.0.0/tests/unit/test_model_resolver.py +77 -0
- lmrelay-0.0.0/tests/unit/test_ping_cmd.py +103 -0
- lmrelay-0.0.0/tests/unit/test_provider_filter.py +178 -0
- lmrelay-0.0.0/tests/unit/test_router_failover.py +154 -0
- lmrelay-0.0.0/tests/unit/test_stream_framing.py +48 -0
- lmrelay-0.0.0/tests/unit/test_toml_config.py +111 -0
- lmrelay-0.0.0/tests/unit/test_translator_anthropic.py +120 -0
- lmrelay-0.0.0/tests/unit/test_translator_ollama.py +100 -0
- lmrelay-0.0.0/tests/unit/test_translator_openai.py +126 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
*.pyo
|
|
4
|
+
.Python
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
.env
|
|
11
|
+
.env.local
|
|
12
|
+
node_modules/
|
|
13
|
+
.DS_Store
|
|
14
|
+
Thumbs.db
|
|
15
|
+
data/
|
|
16
|
+
models/
|
|
17
|
+
*.log
|
|
18
|
+
.idea/
|
|
19
|
+
.vscode/
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
coverage.xml
|
|
25
|
+
htmlcov/
|
|
26
|
+
.lmrelay/
|
|
27
|
+
.git/
|
|
28
|
+
.github/
|
|
29
|
+
tests/
|
|
30
|
+
docs/
|
|
31
|
+
ROADMAP.md
|
|
32
|
+
CHANGELOG.md
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# lmrelay configuration.
|
|
2
|
+
# Copy to ~/.lmrelay/.env and fill in keys for the providers you have.
|
|
3
|
+
|
|
4
|
+
# Server bind
|
|
5
|
+
LMRELAY_HOST=0.0.0.0
|
|
6
|
+
LMRELAY_PORT=11434
|
|
7
|
+
|
|
8
|
+
# State directory (default ~/.lmrelay)
|
|
9
|
+
# LMRELAY_HOME=~/.lmrelay
|
|
10
|
+
|
|
11
|
+
# Security: shared-secret. Empty / unset = auth disabled.
|
|
12
|
+
# LMRELAY_TOKEN=
|
|
13
|
+
|
|
14
|
+
# Behaviour
|
|
15
|
+
LMRELAY_LOG_LEVEL=INFO
|
|
16
|
+
LMRELAY_TELEMETRY=off
|
|
17
|
+
LMRELAY_COOLDOWN_SECONDS=60
|
|
18
|
+
LMRELAY_REQUEST_TIMEOUT=120
|
|
19
|
+
LMRELAY_HEALTH_WINDOW=50
|
|
20
|
+
LMRELAY_MODEL_CATALOG_TTL=21600
|
|
21
|
+
|
|
22
|
+
# CORS (default *)
|
|
23
|
+
# LMRELAY_CORS_ORIGINS=*
|
|
24
|
+
|
|
25
|
+
# Metrics endpoint (default off)
|
|
26
|
+
# LMRELAY_METRICS=off
|
|
27
|
+
|
|
28
|
+
# Comma-separated provider names the router should skip, even when their
|
|
29
|
+
# key is set. Useful for temporarily pausing a flaky provider while
|
|
30
|
+
# leaving its key in the .env. Names: openrouter, groq, nvidia_nim,
|
|
31
|
+
# huggingface, cerebras, cloudflare_wai, gemini, ollama_backend.
|
|
32
|
+
# LMRELAY_DISABLED_PROVIDERS=cloudflare_wai,huggingface
|
|
33
|
+
|
|
34
|
+
# Optional local Ollama as a backend provider
|
|
35
|
+
# OLLAMA_BACKEND_URL=http://127.0.0.1:11435
|
|
36
|
+
|
|
37
|
+
# Provider keys (uncomment + fill).
|
|
38
|
+
# All providers support multi-key: <PREFIX>, <PREFIX>_2, <PREFIX>_3, ...
|
|
39
|
+
|
|
40
|
+
# OPENROUTER_API_KEY=sk-or-v1-...
|
|
41
|
+
# OPENROUTER_API_KEY_2=
|
|
42
|
+
|
|
43
|
+
# GROQ_API_KEY=gsk_...
|
|
44
|
+
# GROQ_API_KEY_2=
|
|
45
|
+
|
|
46
|
+
# NVIDIA_NIM_API_KEY=nvapi-...
|
|
47
|
+
# NVIDIA_NIM_API_KEY_2=
|
|
48
|
+
|
|
49
|
+
# HUGGINGFACE_API_KEY=hf_...
|
|
50
|
+
# HUGGINGFACE_API_KEY_2=
|
|
51
|
+
|
|
52
|
+
# CEREBRAS_API_KEY=csk-...
|
|
53
|
+
# CEREBRAS_API_KEY_2=
|
|
54
|
+
|
|
55
|
+
# CLOUDFLARE_WAI_API_KEY=
|
|
56
|
+
# CLOUDFLARE_ACCOUNT_ID=
|
|
57
|
+
|
|
58
|
+
# Google AI Studio (Gemini). Get a key at https://aistudio.google.com/apikey
|
|
59
|
+
# GEMINI_API_KEY=AIza...
|
|
60
|
+
# GEMINI_API_KEY_2=
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request: {}
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
lint-and-type:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.12"
|
|
15
|
+
- run: pip install -e ".[dev]"
|
|
16
|
+
- run: ruff check .
|
|
17
|
+
- run: mypy lmrelay
|
|
18
|
+
|
|
19
|
+
test:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
strategy:
|
|
22
|
+
fail-fast: false
|
|
23
|
+
matrix:
|
|
24
|
+
python: ["3.11", "3.12", "3.13"]
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: ${{ matrix.python }}
|
|
30
|
+
- run: pip install -e ".[dev]"
|
|
31
|
+
- run: pytest -m "not e2e" --cov=lmrelay --cov-report=term-missing
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags:
|
|
5
|
+
- "*.*.*"
|
|
6
|
+
- "v*"
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
id-token: write
|
|
10
|
+
jobs:
|
|
11
|
+
build-and-publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- run: python -m pip install -U build
|
|
19
|
+
- run: python -m build
|
|
20
|
+
- name: Publish to PyPI
|
|
21
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
22
|
+
with:
|
|
23
|
+
skip-existing: true
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ["v*"]
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
build-and-publish-pypi:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
permissions:
|
|
10
|
+
id-token: write
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- run: pip install --upgrade build
|
|
17
|
+
- run: python -m build
|
|
18
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
19
|
+
|
|
20
|
+
build-and-push-image:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
packages: write
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: docker/setup-qemu-action@v3
|
|
28
|
+
- uses: docker/setup-buildx-action@v3
|
|
29
|
+
- uses: docker/login-action@v3
|
|
30
|
+
with:
|
|
31
|
+
registry: ghcr.io
|
|
32
|
+
username: ${{ github.actor }}
|
|
33
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
34
|
+
- uses: docker/build-push-action@v5
|
|
35
|
+
with:
|
|
36
|
+
context: .
|
|
37
|
+
file: ./Dockerfile
|
|
38
|
+
platforms: linux/amd64,linux/arm64
|
|
39
|
+
push: true
|
|
40
|
+
tags: |
|
|
41
|
+
ghcr.io/${{ github.repository }}:latest
|
|
42
|
+
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
|
lmrelay-0.0.0/.gitignore
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Local lmrelay config — never commit your keys.
|
|
2
|
+
lmrelay.toml
|
|
3
|
+
!lmrelay.toml.example
|
|
4
|
+
|
|
5
|
+
.idea/
|
|
6
|
+
.vscode/
|
|
7
|
+
.cursor/
|
|
8
|
+
.claude/
|
|
9
|
+
.codex
|
|
10
|
+
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
*.egg-info/
|
|
14
|
+
.eggs/
|
|
15
|
+
build/
|
|
16
|
+
dist/
|
|
17
|
+
.venv/
|
|
18
|
+
venv/
|
|
19
|
+
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.pyright/
|
|
24
|
+
.coverage
|
|
25
|
+
htmlcov/
|
|
26
|
+
site/
|
|
27
|
+
|
|
28
|
+
.env
|
|
29
|
+
.env.*
|
|
30
|
+
!.env.example
|
|
31
|
+
|
|
32
|
+
*.md
|
|
33
|
+
!README.md
|
|
34
|
+
!CHANGELOG.md
|
|
35
|
+
!CONTRIBUTING.md
|
|
36
|
+
!docs/**/*.md
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Project renamed `freellama` → `lmrelay`.** Reflects the broader
|
|
12
|
+
scope: balancer / gateway for LLM providers with upcoming profiles
|
|
13
|
+
and tokens, not just a free-tier-only Ollama drop-in.
|
|
14
|
+
- PyPI: `pip install lmrelay`
|
|
15
|
+
- CLI: `lmrelay`, `lmrelay-watcher`
|
|
16
|
+
- Env vars: `FREELLAMA_*` → `LMRELAY_*`
|
|
17
|
+
- State dir: `~/.freellama/` → `~/.lmrelay/`
|
|
18
|
+
- Config file: `freellama.toml` → `lmrelay.toml`
|
|
19
|
+
- Docker prefix: `frl_*` → `lmr_*`
|
|
20
|
+
- systemd: `freellama.service` → `lmrelay.service`
|
|
21
|
+
- HTTP header: `X-Freellama-Request-Id` → `X-Lmrelay-Request-Id`
|
|
22
|
+
- Integration stub-token: `freellama-stub-token` → `lmrelay-stub-token`
|
|
23
|
+
Existing installs must migrate config + state by hand; there is no
|
|
24
|
+
automatic shim.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- Google Gemini as an upstream provider via the OpenAI-compatible endpoint
|
|
28
|
+
(`generativelanguage.googleapis.com/v1beta/openai`). `GEMINI_API_KEY[_N]`
|
|
29
|
+
recognised on startup. Capabilities: chat, stream, tools, embed, vision,
|
|
30
|
+
json. Short names `gemini`, `gemini-flash`, `gemini-pro`, `gemini-2.5`,
|
|
31
|
+
`gemini-2.0`, `gemini-1.5`, `gemini-3` resolve to Gemini models; `gemini`
|
|
32
|
+
is also considered by the `free`, `fast`, `quality`, `coding`, `vision`,
|
|
33
|
+
and `embed` virtual aliases.
|
|
34
|
+
- Catalog entries from GeminiProvider carry a `task` tag (`chat` or
|
|
35
|
+
`embedding`); `core.aliases.model_task()` reads it (or falls back to
|
|
36
|
+
a substring heuristic for other providers). `resolve_alias` filters by
|
|
37
|
+
task so chat aliases never accidentally route to an embedding model.
|
|
38
|
+
- `lmrelay ping` — lists every provider with its enabled/disabled
|
|
39
|
+
status (key configured / explicitly disabled / ready) and, unless
|
|
40
|
+
`--no-probe` is passed, sends a minimal `Reply with one word: pong`
|
|
41
|
+
chat request (max_tokens=4) to the first model of each enabled
|
|
42
|
+
provider. Supports `--provider`, `--no-probe`, `--json`.
|
|
43
|
+
- `LMRELAY_DISABLED_PROVIDERS` env var — comma-separated list of
|
|
44
|
+
provider names that the router will skip even when their key is set.
|
|
45
|
+
Disabled providers are dropped from `app.state.providers` at lifespan
|
|
46
|
+
startup.
|
|
47
|
+
- Per-provider free/paid catalog toggle. Each `[provider.X]` table
|
|
48
|
+
takes four optional fields:
|
|
49
|
+
`include_free` (default `true`), `include_paid` (default `false`),
|
|
50
|
+
`include_extra` (fnmatch globs always included), `exclude`
|
|
51
|
+
(fnmatch globs always dropped — wins over the other rules).
|
|
52
|
+
`BaseProvider.list_models()` is now non-abstract and returns the
|
|
53
|
+
subset that `self.filter.allows()` keeps. Each catalog entry carries
|
|
54
|
+
a `tier` tag (`"free"` or `"paid"`); OpenRouter classifies by the
|
|
55
|
+
`pricing.prompt` field, other providers default to free.
|
|
56
|
+
Added `BaseProvider.list_all_models()` that bypasses the filter.
|
|
57
|
+
- `core.provider_filter.ProviderFilter` + `build_provider_filter`;
|
|
58
|
+
`providers.registry.load_provider_filters()` and a wiring step in
|
|
59
|
+
`build_providers` that applies them.
|
|
60
|
+
|
|
61
|
+
- `lmrelay.toml` config — sectioned by `[provider.X]` with one
|
|
62
|
+
`[[provider.X.keys]]` block per credential. Resolution order:
|
|
63
|
+
`./lmrelay.toml` (local checkout, gitignored) →
|
|
64
|
+
`~/.lmrelay/lmrelay.toml` (installed) → built-in defaults.
|
|
65
|
+
Process env vars override.
|
|
66
|
+
- `lmrelay reload` CLI command + `POST /admin/reload` endpoint.
|
|
67
|
+
Re-reads `lmrelay.toml` and `.env`, rebuilds the key ring + active
|
|
68
|
+
provider list, clears the cooldown matrix. Host/port stay bound.
|
|
69
|
+
Auth via the existing `Authorization: Bearer <LMRELAY_TOKEN>`.
|
|
70
|
+
- `lmrelay.toml.example` template.
|
|
71
|
+
- `deploy/systemd/lmrelay.service` and
|
|
72
|
+
`deploy/systemd/lmrelay-watcher.service` with `ExecReload=`
|
|
73
|
+
wired to `lmrelay reload`; `deploy/README.md` install guide.
|
|
74
|
+
- `docker-compose.yml`: documents `lmrelay.toml` mount via
|
|
75
|
+
`./.lmrelay/`; sets `LMRELAY_HOME=/root/.lmrelay`.
|
|
76
|
+
- Per-key model filter — each `[[provider.X.keys]]` block may carry a
|
|
77
|
+
`models = ["pattern", ...]` allow-list of fnmatch globs (e.g.
|
|
78
|
+
`["*:free"]`, `["meta-llama/*"]`, `["gemini-2.5-*"]`). The router's
|
|
79
|
+
candidate chain only includes a `(provider, key, model)` triple if
|
|
80
|
+
the key's allow-list (when present) matches the model id. Empty /
|
|
81
|
+
missing → no filter. `KeyRing` now exposes `KeyEntry` records via
|
|
82
|
+
`get_entries(provider)` and `find_entry(provider, api_key)`;
|
|
83
|
+
`get(provider)` still returns `list[str]` for backward compatibility.
|
|
84
|
+
`lmrelay keys` shows `label` and `models` columns.
|
|
85
|
+
- `Makefile` with developer targets:
|
|
86
|
+
`install`, `test`, `lint`, `fmt`, `typecheck`, `check`,
|
|
87
|
+
`clean`, `serve`, `reload`, `ping`, `build`, `docker`,
|
|
88
|
+
`compose-up`, `compose-down`. `make` (no args) prints help.
|
|
89
|
+
- `.gitignore`: ignores local `lmrelay.toml`, keeps
|
|
90
|
+
`lmrelay.toml.example` tracked; restructured into IDE / Python
|
|
91
|
+
build / type-checker / dotenv / markdown sections, with broad
|
|
92
|
+
`*.md` ignore + whitelist for `README.md`, `CHANGELOG.md`,
|
|
93
|
+
`CONTRIBUTING.md`, and `docs/**/*.md`.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
- All provider subclasses now call `super().__init__()` in their
|
|
97
|
+
`__init__` to seed `self.catalog` and `self.filter`. Their custom
|
|
98
|
+
`list_models` overrides were removed; tier tagging happens in
|
|
99
|
+
`refresh_catalog`.
|
|
100
|
+
- Gemini chat-model whitelist is now a generation-agnostic regex
|
|
101
|
+
(`gemini-<N>[.<M>]-(flash|pro)[-suffix]*`) instead of a static
|
|
102
|
+
substring list. Gemini 3 (`gemini-3-flash-preview`, `gemini-3-pro`,
|
|
103
|
+
...) and any future `gemini-<N>-flash/pro` lines come online without
|
|
104
|
+
code edits. Short names `gemini`, `gemini-flash`, `gemini-pro` now
|
|
105
|
+
prefer 3-family first; new short name `gemini-3` resolves to the
|
|
106
|
+
3 family. `coding` and `vision` keyword filters extended for
|
|
107
|
+
`gemini-3-flash` / `gemini-3-pro`.
|
|
108
|
+
|
|
109
|
+
### Fixed
|
|
110
|
+
- `gemini-pro` short name no longer falls back to the sunsetted
|
|
111
|
+
`gemini-1.5-pro`; it points at `gemini-3-pro` / `gemini-2.5-pro`.
|
|
112
|
+
- `vision` alias keyword filter narrowed from the over-broad `gemini-2`
|
|
113
|
+
prefix to explicit family entries.
|
|
114
|
+
- Removed dead `model_supports_vision` helper from `providers/gemini.py`.
|
|
115
|
+
- `test_gemini_provider` no longer constructs `GeminiProvider(http=None)`.
|
|
116
|
+
|
|
117
|
+
### Removed
|
|
118
|
+
- Local-only repo-root docs untracked: `CLAUDE.md`, `CONTEXT.md`,
|
|
119
|
+
`ROADMAP.md` (kept on disk, ignored going forward).
|
|
120
|
+
- `install.sh` — redundant with `pipx install lmrelay` /
|
|
121
|
+
`pip install lmrelay` / `uv tool install lmrelay`. The README
|
|
122
|
+
now points to those directly.
|
|
123
|
+
|
|
124
|
+
## [0.0.0] - 2026-05-18 (as `freellama`)
|
|
125
|
+
|
|
126
|
+
### Added
|
|
127
|
+
- Drop-in Ollama-compatible server on port 11434
|
|
128
|
+
- Three wire protocols on one port: Ollama `/api/*`, OpenAI `/v1/*`, Anthropic `/anthropic/v1/*`
|
|
129
|
+
- Seven providers: OpenRouter, Groq, NVIDIA NIM, HuggingFace, Cerebras, Cloudflare Workers AI, local Ollama backend
|
|
130
|
+
- Canonical request/response model (`ChatRequest`, `ChatResponseChunk`, `EmbeddingRequest`)
|
|
131
|
+
- Translators ↔ canonical for all three wire protocols, plus NDJSON/SSE stream framing
|
|
132
|
+
- Failover router with canonical error taxonomy (auth, rate_limit, quota_exhausted, model_not_found, upstream_5xx, timeout, transport, unknown)
|
|
133
|
+
- Per-(provider, key) cooldown matrix with per-class TTL
|
|
134
|
+
- Persistent health tracker (sliding window of 50, JSON-on-disk)
|
|
135
|
+
- Multi-key parsing (`OPENROUTER_API_KEY`, `OPENROUTER_API_KEY_2`, `_3`, ...)
|
|
136
|
+
- Model resolver supporting virtual aliases (`free`, `fast`, `quality`, `coding`, `vision`, `embed`), Ollama-style short names, and pass-through provider IDs
|
|
137
|
+
- CLI: `init`, `serve`, `keys`, `list`, `doctor`, `audit-models`, `bench`, `run`, `bind`, `migrate-ollama`, `telemetry`, `dashboard`, `version`
|
|
138
|
+
- Agent integrations: `claude`, `openclaw`, `codex`, `gemini` (runners) and `aider`, `continue`, `cursor`, `lobechat` (binders)
|
|
139
|
+
- `lmrelay-watcher` daemon that probes head-of-chain on a configurable interval
|
|
140
|
+
- TUI dashboard (`lmrelay dashboard`) with live event tail and provider health bars
|
|
141
|
+
- Bearer-token auth (optional, via `LMRELAY_TOKEN`)
|
|
142
|
+
- Loud LAN security banner when listening on `0.0.0.0` without an auth token
|
|
143
|
+
- Opt-in telemetry (`LMRELAY_TELEMETRY=on`), default OFF
|
|
144
|
+
- Docker image + `docker-compose.yml` (`frl_app` + `frl_watcher` services)
|
|
145
|
+
- GitHub Actions CI: lint + mypy + pytest matrix py3.11/3.12/3.13
|
|
146
|
+
- MIT license
|
lmrelay-0.0.0/Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /opt/app
|
|
4
|
+
|
|
5
|
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
6
|
+
PYTHONUNBUFFERED=1 \
|
|
7
|
+
PIP_NO_CACHE_DIR=1
|
|
8
|
+
|
|
9
|
+
COPY pyproject.toml README.md ./
|
|
10
|
+
COPY lmrelay ./lmrelay
|
|
11
|
+
|
|
12
|
+
RUN pip install --no-cache-dir .
|
|
13
|
+
|
|
14
|
+
EXPOSE 11434
|
|
15
|
+
|
|
16
|
+
CMD ["lmrelay", "serve"]
|
lmrelay-0.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lmrelay contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
lmrelay-0.0.0/Makefile
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# lmrelay developer Makefile.
|
|
2
|
+
#
|
|
3
|
+
# make — show this help
|
|
4
|
+
# make install — create .venv and install with dev extras
|
|
5
|
+
# make check — lint + typecheck + tests
|
|
6
|
+
# make serve — run the dev gateway (reload + bind to 127.0.0.1)
|
|
7
|
+
#
|
|
8
|
+
# Everything respects an externally activated venv; if .venv/bin/python
|
|
9
|
+
# is missing, `make install` creates one.
|
|
10
|
+
|
|
11
|
+
SHELL := /usr/bin/env bash
|
|
12
|
+
|
|
13
|
+
VENV := .venv
|
|
14
|
+
PYTHON := $(VENV)/bin/python
|
|
15
|
+
PIP := $(VENV)/bin/pip
|
|
16
|
+
PYTEST := $(VENV)/bin/pytest
|
|
17
|
+
RUFF := $(VENV)/bin/ruff
|
|
18
|
+
MYPY := $(VENV)/bin/mypy
|
|
19
|
+
LMRELAY := $(VENV)/bin/lmrelay
|
|
20
|
+
|
|
21
|
+
.DEFAULT_GOAL := help
|
|
22
|
+
.PHONY: help install dev test lint fmt format typecheck check clean \
|
|
23
|
+
serve reload ping build docker compose-up compose-down
|
|
24
|
+
|
|
25
|
+
help: ## Show this help
|
|
26
|
+
@awk 'BEGIN {FS = ":.*##"; printf "lmrelay — make targets:\n\n"} \
|
|
27
|
+
/^[a-zA-Z][a-zA-Z0-9_-]*:.*##/ \
|
|
28
|
+
{ printf " \033[1m%-14s\033[0m %s\n", $$1, $$2 }' \
|
|
29
|
+
$(MAKEFILE_LIST)
|
|
30
|
+
|
|
31
|
+
$(VENV): ## (internal) create the dev virtualenv
|
|
32
|
+
python3 -m venv $(VENV)
|
|
33
|
+
$(PIP) install --upgrade pip
|
|
34
|
+
|
|
35
|
+
install: $(VENV) ## Create .venv and install lmrelay with dev extras
|
|
36
|
+
$(PIP) install -e ".[dev]"
|
|
37
|
+
|
|
38
|
+
dev: install ## Alias for install
|
|
39
|
+
|
|
40
|
+
test: ## Run the test suite (quiet)
|
|
41
|
+
$(PYTEST) -q
|
|
42
|
+
|
|
43
|
+
lint: ## ruff check
|
|
44
|
+
$(RUFF) check lmrelay tests
|
|
45
|
+
|
|
46
|
+
fmt: ## ruff --fix + ruff format
|
|
47
|
+
$(RUFF) check --fix lmrelay tests
|
|
48
|
+
$(RUFF) format lmrelay tests
|
|
49
|
+
|
|
50
|
+
format: fmt ## Alias for fmt
|
|
51
|
+
|
|
52
|
+
typecheck: ## mypy
|
|
53
|
+
$(MYPY) lmrelay
|
|
54
|
+
|
|
55
|
+
check: lint typecheck test ## lint + typecheck + tests
|
|
56
|
+
|
|
57
|
+
clean: ## Remove build artifacts and tool caches
|
|
58
|
+
rm -rf build dist *.egg-info \
|
|
59
|
+
.pytest_cache .mypy_cache .ruff_cache \
|
|
60
|
+
htmlcov .coverage coverage.xml
|
|
61
|
+
find . -type d -name __pycache__ -prune -exec rm -rf {} +
|
|
62
|
+
|
|
63
|
+
serve: ## Run lmrelay serve in dev mode (--reload, bound to 127.0.0.1)
|
|
64
|
+
$(LMRELAY) serve --reload --host 127.0.0.1
|
|
65
|
+
|
|
66
|
+
reload: ## Tell a running gateway to re-read its config
|
|
67
|
+
$(LMRELAY) reload
|
|
68
|
+
|
|
69
|
+
ping: ## List providers (enabled/disabled) and ping each enabled one
|
|
70
|
+
$(LMRELAY) ping
|
|
71
|
+
|
|
72
|
+
build: ## Build sdist and wheel into ./dist/
|
|
73
|
+
$(PYTHON) -m pip install --upgrade build
|
|
74
|
+
$(PYTHON) -m build
|
|
75
|
+
|
|
76
|
+
docker: ## Build the local docker image (tag: lmrelay:dev)
|
|
77
|
+
docker build -t lmrelay:dev .
|
|
78
|
+
|
|
79
|
+
compose-up: ## docker compose up -d (frl_app + frl_watcher)
|
|
80
|
+
docker compose up -d
|
|
81
|
+
|
|
82
|
+
compose-down: ## docker compose down
|
|
83
|
+
docker compose down
|