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.
Files changed (135) hide show
  1. lmrelay-0.0.0/.dockerignore +32 -0
  2. lmrelay-0.0.0/.env.example +60 -0
  3. lmrelay-0.0.0/.github/workflows/ci.yml +31 -0
  4. lmrelay-0.0.0/.github/workflows/publish.yml +23 -0
  5. lmrelay-0.0.0/.github/workflows/release.yml +42 -0
  6. lmrelay-0.0.0/.gitignore +36 -0
  7. lmrelay-0.0.0/CHANGELOG.md +146 -0
  8. lmrelay-0.0.0/Dockerfile +16 -0
  9. lmrelay-0.0.0/LICENSE +21 -0
  10. lmrelay-0.0.0/Makefile +83 -0
  11. lmrelay-0.0.0/PKG-INFO +316 -0
  12. lmrelay-0.0.0/README.md +273 -0
  13. lmrelay-0.0.0/deploy/README.md +64 -0
  14. lmrelay-0.0.0/deploy/systemd/lmrelay-watcher.service +29 -0
  15. lmrelay-0.0.0/deploy/systemd/lmrelay.service +42 -0
  16. lmrelay-0.0.0/docker-compose.yml +79 -0
  17. lmrelay-0.0.0/docs/adr/0001-canonical-form-as-routing-pivot.md +51 -0
  18. lmrelay-0.0.0/docs/agents.md +30 -0
  19. lmrelay-0.0.0/docs/api/anthropic.md +39 -0
  20. lmrelay-0.0.0/docs/api/ollama.md +33 -0
  21. lmrelay-0.0.0/docs/api/openai.md +27 -0
  22. lmrelay-0.0.0/docs/architecture.md +54 -0
  23. lmrelay-0.0.0/docs/providers.md +47 -0
  24. lmrelay-0.0.0/docs/security.md +50 -0
  25. lmrelay-0.0.0/lmrelay/__init__.py +5 -0
  26. lmrelay-0.0.0/lmrelay/__main__.py +8 -0
  27. lmrelay-0.0.0/lmrelay/cli/__init__.py +3 -0
  28. lmrelay-0.0.0/lmrelay/cli/cmd_audit.py +96 -0
  29. lmrelay-0.0.0/lmrelay/cli/cmd_bench.py +89 -0
  30. lmrelay-0.0.0/lmrelay/cli/cmd_bind.py +28 -0
  31. lmrelay-0.0.0/lmrelay/cli/cmd_dashboard.py +18 -0
  32. lmrelay-0.0.0/lmrelay/cli/cmd_doctor.py +110 -0
  33. lmrelay-0.0.0/lmrelay/cli/cmd_init.py +70 -0
  34. lmrelay-0.0.0/lmrelay/cli/cmd_keys.py +66 -0
  35. lmrelay-0.0.0/lmrelay/cli/cmd_list.py +93 -0
  36. lmrelay-0.0.0/lmrelay/cli/cmd_migrate_ollama.py +78 -0
  37. lmrelay-0.0.0/lmrelay/cli/cmd_ping.py +244 -0
  38. lmrelay-0.0.0/lmrelay/cli/cmd_reload.py +61 -0
  39. lmrelay-0.0.0/lmrelay/cli/cmd_run.py +30 -0
  40. lmrelay-0.0.0/lmrelay/cli/cmd_serve.py +34 -0
  41. lmrelay-0.0.0/lmrelay/cli/cmd_telemetry.py +34 -0
  42. lmrelay-0.0.0/lmrelay/cli/main.py +67 -0
  43. lmrelay-0.0.0/lmrelay/cli/watcher.py +117 -0
  44. lmrelay-0.0.0/lmrelay/config.py +128 -0
  45. lmrelay-0.0.0/lmrelay/core/__init__.py +3 -0
  46. lmrelay-0.0.0/lmrelay/core/aliases.py +128 -0
  47. lmrelay-0.0.0/lmrelay/core/auth.py +18 -0
  48. lmrelay-0.0.0/lmrelay/core/catalog_index.py +104 -0
  49. lmrelay-0.0.0/lmrelay/core/cooldown.py +58 -0
  50. lmrelay-0.0.0/lmrelay/core/errors.py +27 -0
  51. lmrelay-0.0.0/lmrelay/core/events.py +53 -0
  52. lmrelay-0.0.0/lmrelay/core/health.py +61 -0
  53. lmrelay-0.0.0/lmrelay/core/keys.py +205 -0
  54. lmrelay-0.0.0/lmrelay/core/model_catalog.py +57 -0
  55. lmrelay-0.0.0/lmrelay/core/model_resolver.py +66 -0
  56. lmrelay-0.0.0/lmrelay/core/paths.py +37 -0
  57. lmrelay-0.0.0/lmrelay/core/provider_filter.py +59 -0
  58. lmrelay-0.0.0/lmrelay/core/router.py +181 -0
  59. lmrelay-0.0.0/lmrelay/core/telemetry.py +89 -0
  60. lmrelay-0.0.0/lmrelay/core/toml_config.py +66 -0
  61. lmrelay-0.0.0/lmrelay/core/types.py +65 -0
  62. lmrelay-0.0.0/lmrelay/integrations/__init__.py +3 -0
  63. lmrelay-0.0.0/lmrelay/integrations/aider.py +33 -0
  64. lmrelay-0.0.0/lmrelay/integrations/base.py +27 -0
  65. lmrelay-0.0.0/lmrelay/integrations/claude_code.py +42 -0
  66. lmrelay-0.0.0/lmrelay/integrations/codex.py +44 -0
  67. lmrelay-0.0.0/lmrelay/integrations/continue_.py +44 -0
  68. lmrelay-0.0.0/lmrelay/integrations/cursor.py +31 -0
  69. lmrelay-0.0.0/lmrelay/integrations/gemini.py +43 -0
  70. lmrelay-0.0.0/lmrelay/integrations/lobechat.py +35 -0
  71. lmrelay-0.0.0/lmrelay/integrations/openclaw.py +54 -0
  72. lmrelay-0.0.0/lmrelay/integrations/registry.py +25 -0
  73. lmrelay-0.0.0/lmrelay/logging_setup.py +24 -0
  74. lmrelay-0.0.0/lmrelay/providers/__init__.py +3 -0
  75. lmrelay-0.0.0/lmrelay/providers/base.py +67 -0
  76. lmrelay-0.0.0/lmrelay/providers/cerebras.py +64 -0
  77. lmrelay-0.0.0/lmrelay/providers/cloudflare_wai.py +106 -0
  78. lmrelay-0.0.0/lmrelay/providers/gemini.py +144 -0
  79. lmrelay-0.0.0/lmrelay/providers/groq.py +71 -0
  80. lmrelay-0.0.0/lmrelay/providers/huggingface.py +80 -0
  81. lmrelay-0.0.0/lmrelay/providers/nvidia_nim.py +80 -0
  82. lmrelay-0.0.0/lmrelay/providers/ollama_backend.py +173 -0
  83. lmrelay-0.0.0/lmrelay/providers/openai_compat.py +189 -0
  84. lmrelay-0.0.0/lmrelay/providers/openrouter.py +118 -0
  85. lmrelay-0.0.0/lmrelay/providers/registry.py +78 -0
  86. lmrelay-0.0.0/lmrelay/server/__init__.py +3 -0
  87. lmrelay-0.0.0/lmrelay/server/app.py +139 -0
  88. lmrelay-0.0.0/lmrelay/server/errors.py +60 -0
  89. lmrelay-0.0.0/lmrelay/server/middleware.py +45 -0
  90. lmrelay-0.0.0/lmrelay/server/routes_admin.py +62 -0
  91. lmrelay-0.0.0/lmrelay/server/routes_anthropic.py +36 -0
  92. lmrelay-0.0.0/lmrelay/server/routes_health.py +49 -0
  93. lmrelay-0.0.0/lmrelay/server/routes_ollama.py +224 -0
  94. lmrelay-0.0.0/lmrelay/server/routes_openai.py +129 -0
  95. lmrelay-0.0.0/lmrelay/translators/__init__.py +3 -0
  96. lmrelay-0.0.0/lmrelay/translators/anthropic_to_canonical.py +117 -0
  97. lmrelay-0.0.0/lmrelay/translators/canonical_to_anthropic.py +166 -0
  98. lmrelay-0.0.0/lmrelay/translators/canonical_to_ollama.py +124 -0
  99. lmrelay-0.0.0/lmrelay/translators/canonical_to_openai.py +178 -0
  100. lmrelay-0.0.0/lmrelay/translators/ollama_to_canonical.py +79 -0
  101. lmrelay-0.0.0/lmrelay/translators/openai_to_canonical.py +63 -0
  102. lmrelay-0.0.0/lmrelay/translators/stream_framing.py +37 -0
  103. lmrelay-0.0.0/lmrelay/tui/__init__.py +3 -0
  104. lmrelay-0.0.0/lmrelay/tui/dashboard.py +104 -0
  105. lmrelay-0.0.0/lmrelay.toml.example +106 -0
  106. lmrelay-0.0.0/pyproject.toml +100 -0
  107. lmrelay-0.0.0/tests/__init__.py +0 -0
  108. lmrelay-0.0.0/tests/conftest.py +30 -0
  109. lmrelay-0.0.0/tests/integration/__init__.py +0 -0
  110. lmrelay-0.0.0/tests/integration/conftest.py +59 -0
  111. lmrelay-0.0.0/tests/integration/test_admin_reload.py +102 -0
  112. lmrelay-0.0.0/tests/integration/test_api_anthropic.py +42 -0
  113. lmrelay-0.0.0/tests/integration/test_api_ollama.py +97 -0
  114. lmrelay-0.0.0/tests/integration/test_api_openai.py +72 -0
  115. lmrelay-0.0.0/tests/integration/test_auth.py +37 -0
  116. lmrelay-0.0.0/tests/integration/test_disabled_providers.py +26 -0
  117. lmrelay-0.0.0/tests/integration/test_streaming.py +52 -0
  118. lmrelay-0.0.0/tests/unit/__init__.py +0 -0
  119. lmrelay-0.0.0/tests/unit/test_aliases.py +48 -0
  120. lmrelay-0.0.0/tests/unit/test_config_disabled.py +37 -0
  121. lmrelay-0.0.0/tests/unit/test_cooldown.py +55 -0
  122. lmrelay-0.0.0/tests/unit/test_gemini_provider.py +143 -0
  123. lmrelay-0.0.0/tests/unit/test_health.py +40 -0
  124. lmrelay-0.0.0/tests/unit/test_key_filter.py +140 -0
  125. lmrelay-0.0.0/tests/unit/test_keys_parsing.py +62 -0
  126. lmrelay-0.0.0/tests/unit/test_keys_toml.py +86 -0
  127. lmrelay-0.0.0/tests/unit/test_model_resolver.py +77 -0
  128. lmrelay-0.0.0/tests/unit/test_ping_cmd.py +103 -0
  129. lmrelay-0.0.0/tests/unit/test_provider_filter.py +178 -0
  130. lmrelay-0.0.0/tests/unit/test_router_failover.py +154 -0
  131. lmrelay-0.0.0/tests/unit/test_stream_framing.py +48 -0
  132. lmrelay-0.0.0/tests/unit/test_toml_config.py +111 -0
  133. lmrelay-0.0.0/tests/unit/test_translator_anthropic.py +120 -0
  134. lmrelay-0.0.0/tests/unit/test_translator_ollama.py +100 -0
  135. 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 }}
@@ -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
@@ -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