llms-py 2.0.25__tar.gz → 2.0.26__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.
- {llms_py-2.0.25/llms_py.egg-info → llms_py-2.0.26}/PKG-INFO +59 -51
- {llms_py-2.0.25 → llms_py-2.0.26}/README.md +58 -50
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/llms.json +9 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/main.py +254 -4
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/App.mjs +7 -2
- llms_py-2.0.26/llms/ui/Avatar.mjs +85 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/Main.mjs +8 -5
- llms_py-2.0.26/llms/ui/OAuthSignIn.mjs +92 -0
- llms_py-2.0.26/llms/ui/ai.mjs +144 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/app.css +36 -0
- {llms_py-2.0.25 → llms_py-2.0.26/llms_py.egg-info}/PKG-INFO +59 -51
- {llms_py-2.0.25 → llms_py-2.0.26}/llms_py.egg-info/SOURCES.txt +1 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/pyproject.toml +1 -1
- {llms_py-2.0.25 → llms_py-2.0.26}/setup.py +1 -1
- llms_py-2.0.25/llms/ui/Avatar.mjs +0 -28
- llms_py-2.0.25/llms/ui/ai.mjs +0 -81
- {llms_py-2.0.25 → llms_py-2.0.26}/LICENSE +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/MANIFEST.in +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/__init__.py +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/__main__.py +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/index.html +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/Analytics.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/Brand.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/ChatPrompt.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/ModelSelector.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/ProviderIcon.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/ProviderStatus.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/Recents.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/SettingsDialog.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/Sidebar.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/SignIn.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/SystemPromptEditor.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/SystemPromptSelector.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/Welcome.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/fav.svg +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/lib/chart.js +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/lib/charts.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/lib/color.js +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/lib/highlight.min.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/lib/idb.min.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/lib/marked.min.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/lib/servicestack-client.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/lib/servicestack-vue.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/lib/vue-router.min.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/lib/vue.min.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/lib/vue.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/markdown.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/tailwind.input.css +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/threadStore.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/typography.css +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui/utils.mjs +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms/ui.json +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms_py.egg-info/dependency_links.txt +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms_py.egg-info/entry_points.txt +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms_py.egg-info/not-zip-safe +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms_py.egg-info/requires.txt +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/llms_py.egg-info/top_level.txt +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/requirements.txt +0 -0
- {llms_py-2.0.25 → llms_py-2.0.26}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: llms-py
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.26
|
|
4
4
|
Summary: A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers
|
|
5
5
|
Home-page: https://github.com/ServiceStack/llms
|
|
6
6
|
Author: ServiceStack
|
|
@@ -111,56 +111,7 @@ test the response times for all configured providers and models, the results of
|
|
|
111
111
|
pip install llms-py
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
**a) Simple - Run in a Docker container:**
|
|
117
|
-
|
|
118
|
-
Run the server on port `8000`:
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
docker run -p 8000:8000 -e GROQ_API_KEY=$GROQ_API_KEY ghcr.io/servicestack/llms:latest
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Get the latest version:
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
docker pull ghcr.io/servicestack/llms:latest
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Use custom `llms.json` and `ui.json` config files outside of the container (auto created if they don't exist):
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
docker run -p 8000:8000 -e GROQ_API_KEY=$GROQ_API_KEY \
|
|
134
|
-
-v ~/.llms:/home/llms/.llms \
|
|
135
|
-
ghcr.io/servicestack/llms:latest
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
**b) Recommended - Use Docker Compose:**
|
|
139
|
-
|
|
140
|
-
Download and use [docker-compose.yml](https://raw.githubusercontent.com/ServiceStack/llms/refs/heads/main/docker-compose.yml):
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
curl -O https://raw.githubusercontent.com/ServiceStack/llms/refs/heads/main/docker-compose.yml
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Update API Keys in `docker-compose.yml` then start the server:
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
docker-compose up -d
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
**c) Build and run local Docker image from source:**
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
git clone https://github.com/ServiceStack/llms
|
|
156
|
-
|
|
157
|
-
docker-compose -f docker-compose.local.yml up -d --build
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
After the container starts, you can access the UI and API at `http://localhost:8000`.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
See [DOCKER.md](DOCKER.md) for detailed instructions on customizing configuration files.
|
|
114
|
+
- [Using Docker](#using-docker)
|
|
164
115
|
|
|
165
116
|
## Quick Start
|
|
166
117
|
|
|
@@ -224,6 +175,63 @@ llms --disable openrouter_free codestral google_free groq
|
|
|
224
175
|
llms --enable openrouter anthropic google openai grok z.ai qwen mistral
|
|
225
176
|
```
|
|
226
177
|
|
|
178
|
+
## Using Docker
|
|
179
|
+
|
|
180
|
+
#### a) Simple - Run in a Docker container:
|
|
181
|
+
|
|
182
|
+
Run the server on port `8000`:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
docker run -p 8000:8000 -e GROQ_API_KEY=$GROQ_API_KEY ghcr.io/servicestack/llms:latest
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Get the latest version:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
docker pull ghcr.io/servicestack/llms:latest
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Use custom `llms.json` and `ui.json` config files outside of the container (auto created if they don't exist):
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
docker run -p 8000:8000 -e GROQ_API_KEY=$GROQ_API_KEY \
|
|
198
|
+
-v ~/.llms:/home/llms/.llms \
|
|
199
|
+
ghcr.io/servicestack/llms:latest
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### b) Recommended - Use Docker Compose:
|
|
203
|
+
|
|
204
|
+
Download and use [docker-compose.yml](https://raw.githubusercontent.com/ServiceStack/llms/refs/heads/main/docker-compose.yml):
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
curl -O https://raw.githubusercontent.com/ServiceStack/llms/refs/heads/main/docker-compose.yml
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Update API Keys in `docker-compose.yml` then start the server:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
docker-compose up -d
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### c) Build and run local Docker image from source:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
git clone https://github.com/ServiceStack/llms
|
|
220
|
+
|
|
221
|
+
docker-compose -f docker-compose.local.yml up -d --build
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
After the container starts, you can access the UI and API at `http://localhost:8000`.
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
See [DOCKER.md](DOCKER.md) for detailed instructions on customizing configuration files.
|
|
228
|
+
|
|
229
|
+
## GitHub OAuth Authentication
|
|
230
|
+
|
|
231
|
+
llms.py supports optional GitHub OAuth authentication to secure your web UI and API endpoints. When enabled, users must sign in with their GitHub account before accessing the application.
|
|
232
|
+
|
|
233
|
+
See [GITHUB_OAUTH_SETUP.md](GITHUB_OAUTH_SETUP.md) for detailed setup instructions.
|
|
234
|
+
|
|
227
235
|
## Configuration
|
|
228
236
|
|
|
229
237
|
The configuration file [llms.json](llms/llms.json) is saved to `~/.llms/llms.json` and defines available providers, models, and default settings. Key sections:
|
|
@@ -71,56 +71,7 @@ test the response times for all configured providers and models, the results of
|
|
|
71
71
|
pip install llms-py
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
**a) Simple - Run in a Docker container:**
|
|
77
|
-
|
|
78
|
-
Run the server on port `8000`:
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
docker run -p 8000:8000 -e GROQ_API_KEY=$GROQ_API_KEY ghcr.io/servicestack/llms:latest
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Get the latest version:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
docker pull ghcr.io/servicestack/llms:latest
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Use custom `llms.json` and `ui.json` config files outside of the container (auto created if they don't exist):
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
docker run -p 8000:8000 -e GROQ_API_KEY=$GROQ_API_KEY \
|
|
94
|
-
-v ~/.llms:/home/llms/.llms \
|
|
95
|
-
ghcr.io/servicestack/llms:latest
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
**b) Recommended - Use Docker Compose:**
|
|
99
|
-
|
|
100
|
-
Download and use [docker-compose.yml](https://raw.githubusercontent.com/ServiceStack/llms/refs/heads/main/docker-compose.yml):
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
curl -O https://raw.githubusercontent.com/ServiceStack/llms/refs/heads/main/docker-compose.yml
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Update API Keys in `docker-compose.yml` then start the server:
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
docker-compose up -d
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
**c) Build and run local Docker image from source:**
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
git clone https://github.com/ServiceStack/llms
|
|
116
|
-
|
|
117
|
-
docker-compose -f docker-compose.local.yml up -d --build
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
After the container starts, you can access the UI and API at `http://localhost:8000`.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
See [DOCKER.md](DOCKER.md) for detailed instructions on customizing configuration files.
|
|
74
|
+
- [Using Docker](#using-docker)
|
|
124
75
|
|
|
125
76
|
## Quick Start
|
|
126
77
|
|
|
@@ -184,6 +135,63 @@ llms --disable openrouter_free codestral google_free groq
|
|
|
184
135
|
llms --enable openrouter anthropic google openai grok z.ai qwen mistral
|
|
185
136
|
```
|
|
186
137
|
|
|
138
|
+
## Using Docker
|
|
139
|
+
|
|
140
|
+
#### a) Simple - Run in a Docker container:
|
|
141
|
+
|
|
142
|
+
Run the server on port `8000`:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
docker run -p 8000:8000 -e GROQ_API_KEY=$GROQ_API_KEY ghcr.io/servicestack/llms:latest
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Get the latest version:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
docker pull ghcr.io/servicestack/llms:latest
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Use custom `llms.json` and `ui.json` config files outside of the container (auto created if they don't exist):
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
docker run -p 8000:8000 -e GROQ_API_KEY=$GROQ_API_KEY \
|
|
158
|
+
-v ~/.llms:/home/llms/.llms \
|
|
159
|
+
ghcr.io/servicestack/llms:latest
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### b) Recommended - Use Docker Compose:
|
|
163
|
+
|
|
164
|
+
Download and use [docker-compose.yml](https://raw.githubusercontent.com/ServiceStack/llms/refs/heads/main/docker-compose.yml):
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
curl -O https://raw.githubusercontent.com/ServiceStack/llms/refs/heads/main/docker-compose.yml
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Update API Keys in `docker-compose.yml` then start the server:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
docker-compose up -d
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### c) Build and run local Docker image from source:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
git clone https://github.com/ServiceStack/llms
|
|
180
|
+
|
|
181
|
+
docker-compose -f docker-compose.local.yml up -d --build
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
After the container starts, you can access the UI and API at `http://localhost:8000`.
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
See [DOCKER.md](DOCKER.md) for detailed instructions on customizing configuration files.
|
|
188
|
+
|
|
189
|
+
## GitHub OAuth Authentication
|
|
190
|
+
|
|
191
|
+
llms.py supports optional GitHub OAuth authentication to secure your web UI and API endpoints. When enabled, users must sign in with their GitHub account before accessing the application.
|
|
192
|
+
|
|
193
|
+
See [GITHUB_OAUTH_SETUP.md](GITHUB_OAUTH_SETUP.md) for detailed setup instructions.
|
|
194
|
+
|
|
187
195
|
## Configuration
|
|
188
196
|
|
|
189
197
|
The configuration file [llms.json](llms/llms.json) is saved to `~/.llms/llms.json` and defines available providers, models, and default settings. Key sections:
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
+
"auth": {
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"github": {
|
|
5
|
+
"client_id": "$GITHUB_CLIENT_ID",
|
|
6
|
+
"client_secret": "$GITHUB_CLIENT_SECRET",
|
|
7
|
+
"redirect_uri": "http://localhost:8000/auth/github/callback"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
2
10
|
"defaults": {
|
|
3
11
|
"headers": {
|
|
4
12
|
"Content-Type": "application/json",
|
|
@@ -113,6 +121,7 @@
|
|
|
113
121
|
"mai-ds-r1": "microsoft/mai-ds-r1:free",
|
|
114
122
|
"llama3.3:70b": "meta-llama/llama-3.3-70b-instruct:free",
|
|
115
123
|
"nemotron-nano:9b": "nvidia/nemotron-nano-9b-v2:free",
|
|
124
|
+
"nemotron-nano:12b":"nvidia/nemotron-nano-12b-v2-vl:free",
|
|
116
125
|
"deepseek-r1-distill-llama:70b": "deepseek/deepseek-r1-distill-llama-70b:free",
|
|
117
126
|
"gpt-oss:20b": "openai/gpt-oss-20b:free",
|
|
118
127
|
"mistral-small3.2:24b": "mistralai/mistral-small-3.2-24b-instruct:free",
|
|
@@ -14,7 +14,8 @@ import mimetypes
|
|
|
14
14
|
import traceback
|
|
15
15
|
import sys
|
|
16
16
|
import site
|
|
17
|
-
|
|
17
|
+
import secrets
|
|
18
|
+
from urllib.parse import parse_qs, urlencode
|
|
18
19
|
|
|
19
20
|
import aiohttp
|
|
20
21
|
from aiohttp import web
|
|
@@ -22,7 +23,7 @@ from aiohttp import web
|
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
|
|
24
25
|
|
|
25
|
-
VERSION = "2.0.
|
|
26
|
+
VERSION = "2.0.26"
|
|
26
27
|
_ROOT = None
|
|
27
28
|
g_config_path = None
|
|
28
29
|
g_ui_path = None
|
|
@@ -31,6 +32,8 @@ g_handlers = {}
|
|
|
31
32
|
g_verbose = False
|
|
32
33
|
g_logprefix=""
|
|
33
34
|
g_default_model=""
|
|
35
|
+
g_sessions = {} # OAuth session storage: {session_token: {userId, userName, displayName, profileUrl, email, created}}
|
|
36
|
+
g_oauth_states = {} # CSRF protection: {state: {created, redirect_uri}}
|
|
34
37
|
|
|
35
38
|
def _log(message):
|
|
36
39
|
"""Helper method for logging from the global polling task."""
|
|
@@ -1456,9 +1459,61 @@ def main():
|
|
|
1456
1459
|
print(f"UI not found at {g_ui_path}")
|
|
1457
1460
|
exit(1)
|
|
1458
1461
|
|
|
1462
|
+
# Validate auth configuration if enabled
|
|
1463
|
+
auth_enabled = g_config.get('auth', {}).get('enabled', False)
|
|
1464
|
+
if auth_enabled:
|
|
1465
|
+
github_config = g_config.get('auth', {}).get('github', {})
|
|
1466
|
+
client_id = github_config.get('client_id', '')
|
|
1467
|
+
client_secret = github_config.get('client_secret', '')
|
|
1468
|
+
|
|
1469
|
+
# Expand environment variables
|
|
1470
|
+
if client_id.startswith('$'):
|
|
1471
|
+
client_id = os.environ.get(client_id[1:], '')
|
|
1472
|
+
if client_secret.startswith('$'):
|
|
1473
|
+
client_secret = os.environ.get(client_secret[1:], '')
|
|
1474
|
+
|
|
1475
|
+
if not client_id or not client_secret:
|
|
1476
|
+
print("ERROR: Authentication is enabled but GitHub OAuth is not properly configured.")
|
|
1477
|
+
print("Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables,")
|
|
1478
|
+
print("or disable authentication by setting 'auth.enabled' to false in llms.json")
|
|
1479
|
+
exit(1)
|
|
1480
|
+
|
|
1481
|
+
_log("Authentication enabled - GitHub OAuth configured")
|
|
1482
|
+
|
|
1459
1483
|
app = web.Application()
|
|
1460
1484
|
|
|
1485
|
+
# Authentication middleware helper
|
|
1486
|
+
def check_auth(request):
|
|
1487
|
+
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
1488
|
+
if not auth_enabled:
|
|
1489
|
+
return True, None
|
|
1490
|
+
|
|
1491
|
+
# Check for OAuth session token
|
|
1492
|
+
session_token = request.query.get('session') or request.headers.get('X-Session-Token')
|
|
1493
|
+
if session_token and session_token in g_sessions:
|
|
1494
|
+
return True, g_sessions[session_token]
|
|
1495
|
+
|
|
1496
|
+
# Check for API key
|
|
1497
|
+
auth_header = request.headers.get('Authorization', '')
|
|
1498
|
+
if auth_header.startswith('Bearer '):
|
|
1499
|
+
api_key = auth_header[7:]
|
|
1500
|
+
if api_key:
|
|
1501
|
+
return True, {"authProvider": "apikey"}
|
|
1502
|
+
|
|
1503
|
+
return False, None
|
|
1504
|
+
|
|
1461
1505
|
async def chat_handler(request):
|
|
1506
|
+
# Check authentication if enabled
|
|
1507
|
+
is_authenticated, user_data = check_auth(request)
|
|
1508
|
+
if not is_authenticated:
|
|
1509
|
+
return web.json_response({
|
|
1510
|
+
"error": {
|
|
1511
|
+
"message": "Authentication required",
|
|
1512
|
+
"type": "authentication_error",
|
|
1513
|
+
"code": "unauthorized"
|
|
1514
|
+
}
|
|
1515
|
+
}, status=401)
|
|
1516
|
+
|
|
1462
1517
|
try:
|
|
1463
1518
|
chat = await request.json()
|
|
1464
1519
|
response = await chat_completion(chat)
|
|
@@ -1504,6 +1559,198 @@ def main():
|
|
|
1504
1559
|
})
|
|
1505
1560
|
app.router.add_post('/providers/{provider}', provider_handler)
|
|
1506
1561
|
|
|
1562
|
+
# OAuth handlers
|
|
1563
|
+
async def github_auth_handler(request):
|
|
1564
|
+
"""Initiate GitHub OAuth flow"""
|
|
1565
|
+
if 'auth' not in g_config or 'github' not in g_config['auth']:
|
|
1566
|
+
return web.json_response({"error": "GitHub OAuth not configured"}, status=500)
|
|
1567
|
+
|
|
1568
|
+
auth_config = g_config['auth']['github']
|
|
1569
|
+
client_id = auth_config.get('client_id', '')
|
|
1570
|
+
redirect_uri = auth_config.get('redirect_uri', '')
|
|
1571
|
+
|
|
1572
|
+
# Expand environment variables
|
|
1573
|
+
if client_id.startswith('$'):
|
|
1574
|
+
client_id = os.environ.get(client_id[1:], '')
|
|
1575
|
+
if redirect_uri.startswith('$'):
|
|
1576
|
+
redirect_uri = os.environ.get(redirect_uri[1:], '')
|
|
1577
|
+
|
|
1578
|
+
if not client_id:
|
|
1579
|
+
return web.json_response({"error": "GitHub client_id not configured"}, status=500)
|
|
1580
|
+
|
|
1581
|
+
# Generate CSRF state token
|
|
1582
|
+
state = secrets.token_urlsafe(32)
|
|
1583
|
+
g_oauth_states[state] = {
|
|
1584
|
+
'created': time.time(),
|
|
1585
|
+
'redirect_uri': redirect_uri
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
# Clean up old states (older than 10 minutes)
|
|
1589
|
+
current_time = time.time()
|
|
1590
|
+
expired_states = [s for s, data in g_oauth_states.items() if current_time - data['created'] > 600]
|
|
1591
|
+
for s in expired_states:
|
|
1592
|
+
del g_oauth_states[s]
|
|
1593
|
+
|
|
1594
|
+
# Build GitHub authorization URL
|
|
1595
|
+
params = {
|
|
1596
|
+
'client_id': client_id,
|
|
1597
|
+
'redirect_uri': redirect_uri,
|
|
1598
|
+
'state': state,
|
|
1599
|
+
'scope': 'read:user user:email'
|
|
1600
|
+
}
|
|
1601
|
+
auth_url = f"https://github.com/login/oauth/authorize?{urlencode(params)}"
|
|
1602
|
+
|
|
1603
|
+
return web.HTTPFound(auth_url)
|
|
1604
|
+
|
|
1605
|
+
async def github_callback_handler(request):
|
|
1606
|
+
"""Handle GitHub OAuth callback"""
|
|
1607
|
+
code = request.query.get('code')
|
|
1608
|
+
state = request.query.get('state')
|
|
1609
|
+
|
|
1610
|
+
if not code or not state:
|
|
1611
|
+
return web.Response(text="Missing code or state parameter", status=400)
|
|
1612
|
+
|
|
1613
|
+
# Verify state token (CSRF protection)
|
|
1614
|
+
if state not in g_oauth_states:
|
|
1615
|
+
return web.Response(text="Invalid state parameter", status=400)
|
|
1616
|
+
|
|
1617
|
+
state_data = g_oauth_states.pop(state)
|
|
1618
|
+
|
|
1619
|
+
if 'auth' not in g_config or 'github' not in g_config['auth']:
|
|
1620
|
+
return web.json_response({"error": "GitHub OAuth not configured"}, status=500)
|
|
1621
|
+
|
|
1622
|
+
auth_config = g_config['auth']['github']
|
|
1623
|
+
client_id = auth_config.get('client_id', '')
|
|
1624
|
+
client_secret = auth_config.get('client_secret', '')
|
|
1625
|
+
redirect_uri = auth_config.get('redirect_uri', '')
|
|
1626
|
+
|
|
1627
|
+
# Expand environment variables
|
|
1628
|
+
if client_id.startswith('$'):
|
|
1629
|
+
client_id = os.environ.get(client_id[1:], '')
|
|
1630
|
+
if client_secret.startswith('$'):
|
|
1631
|
+
client_secret = os.environ.get(client_secret[1:], '')
|
|
1632
|
+
if redirect_uri.startswith('$'):
|
|
1633
|
+
redirect_uri = os.environ.get(redirect_uri[1:], '')
|
|
1634
|
+
|
|
1635
|
+
if not client_id or not client_secret:
|
|
1636
|
+
return web.json_response({"error": "GitHub OAuth credentials not configured"}, status=500)
|
|
1637
|
+
|
|
1638
|
+
# Exchange code for access token
|
|
1639
|
+
async with aiohttp.ClientSession() as session:
|
|
1640
|
+
token_url = "https://github.com/login/oauth/access_token"
|
|
1641
|
+
token_data = {
|
|
1642
|
+
'client_id': client_id,
|
|
1643
|
+
'client_secret': client_secret,
|
|
1644
|
+
'code': code,
|
|
1645
|
+
'redirect_uri': redirect_uri
|
|
1646
|
+
}
|
|
1647
|
+
headers = {'Accept': 'application/json'}
|
|
1648
|
+
|
|
1649
|
+
async with session.post(token_url, data=token_data, headers=headers) as resp:
|
|
1650
|
+
token_response = await resp.json()
|
|
1651
|
+
access_token = token_response.get('access_token')
|
|
1652
|
+
|
|
1653
|
+
if not access_token:
|
|
1654
|
+
error = token_response.get('error_description', 'Failed to get access token')
|
|
1655
|
+
return web.Response(text=f"OAuth error: {error}", status=400)
|
|
1656
|
+
|
|
1657
|
+
# Fetch user info
|
|
1658
|
+
user_url = "https://api.github.com/user"
|
|
1659
|
+
headers = {
|
|
1660
|
+
"Authorization": f"Bearer {access_token}",
|
|
1661
|
+
"Accept": "application/json"
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
async with session.get(user_url, headers=headers) as resp:
|
|
1665
|
+
user_data = await resp.json()
|
|
1666
|
+
|
|
1667
|
+
# Create session
|
|
1668
|
+
session_token = secrets.token_urlsafe(32)
|
|
1669
|
+
g_sessions[session_token] = {
|
|
1670
|
+
"userId": str(user_data.get('id', '')),
|
|
1671
|
+
"userName": user_data.get('login', ''),
|
|
1672
|
+
"displayName": user_data.get('name', ''),
|
|
1673
|
+
"profileUrl": user_data.get('avatar_url', ''),
|
|
1674
|
+
"email": user_data.get('email', ''),
|
|
1675
|
+
"created": time.time()
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
# Redirect to UI with session token
|
|
1679
|
+
return web.HTTPFound(f"/?session={session_token}")
|
|
1680
|
+
|
|
1681
|
+
async def session_handler(request):
|
|
1682
|
+
"""Validate and return session info"""
|
|
1683
|
+
session_token = request.query.get('session') or request.headers.get('X-Session-Token')
|
|
1684
|
+
|
|
1685
|
+
if not session_token or session_token not in g_sessions:
|
|
1686
|
+
return web.json_response({"error": "Invalid or expired session"}, status=401)
|
|
1687
|
+
|
|
1688
|
+
session_data = g_sessions[session_token]
|
|
1689
|
+
|
|
1690
|
+
# Clean up old sessions (older than 24 hours)
|
|
1691
|
+
current_time = time.time()
|
|
1692
|
+
expired_sessions = [token for token, data in g_sessions.items() if current_time - data['created'] > 86400]
|
|
1693
|
+
for token in expired_sessions:
|
|
1694
|
+
del g_sessions[token]
|
|
1695
|
+
|
|
1696
|
+
return web.json_response({
|
|
1697
|
+
**session_data,
|
|
1698
|
+
"sessionToken": session_token
|
|
1699
|
+
})
|
|
1700
|
+
|
|
1701
|
+
async def logout_handler(request):
|
|
1702
|
+
"""End OAuth session"""
|
|
1703
|
+
session_token = request.query.get('session') or request.headers.get('X-Session-Token')
|
|
1704
|
+
|
|
1705
|
+
if session_token and session_token in g_sessions:
|
|
1706
|
+
del g_sessions[session_token]
|
|
1707
|
+
|
|
1708
|
+
return web.json_response({"success": True})
|
|
1709
|
+
|
|
1710
|
+
async def auth_handler(request):
|
|
1711
|
+
"""Check authentication status and return user info"""
|
|
1712
|
+
# Check for OAuth session token
|
|
1713
|
+
session_token = request.query.get('session') or request.headers.get('X-Session-Token')
|
|
1714
|
+
|
|
1715
|
+
if session_token and session_token in g_sessions:
|
|
1716
|
+
session_data = g_sessions[session_token]
|
|
1717
|
+
return web.json_response({
|
|
1718
|
+
"userId": session_data.get("userId", ""),
|
|
1719
|
+
"userName": session_data.get("userName", ""),
|
|
1720
|
+
"displayName": session_data.get("displayName", ""),
|
|
1721
|
+
"profileUrl": session_data.get("profileUrl", ""),
|
|
1722
|
+
"authProvider": "github"
|
|
1723
|
+
})
|
|
1724
|
+
|
|
1725
|
+
# Check for API key in Authorization header
|
|
1726
|
+
# auth_header = request.headers.get('Authorization', '')
|
|
1727
|
+
# if auth_header.startswith('Bearer '):
|
|
1728
|
+
# # For API key auth, return a basic response
|
|
1729
|
+
# # You can customize this based on your API key validation logic
|
|
1730
|
+
# api_key = auth_header[7:]
|
|
1731
|
+
# if api_key: # Add your API key validation logic here
|
|
1732
|
+
# return web.json_response({
|
|
1733
|
+
# "userId": "1",
|
|
1734
|
+
# "userName": "apiuser",
|
|
1735
|
+
# "displayName": "API User",
|
|
1736
|
+
# "profileUrl": "",
|
|
1737
|
+
# "authProvider": "apikey"
|
|
1738
|
+
# })
|
|
1739
|
+
|
|
1740
|
+
# Not authenticated - return error in expected format
|
|
1741
|
+
return web.json_response({
|
|
1742
|
+
"responseStatus": {
|
|
1743
|
+
"errorCode": "Unauthorized",
|
|
1744
|
+
"message": "Not authenticated"
|
|
1745
|
+
}
|
|
1746
|
+
}, status=401)
|
|
1747
|
+
|
|
1748
|
+
app.router.add_get('/auth', auth_handler)
|
|
1749
|
+
app.router.add_get('/auth/github', github_auth_handler)
|
|
1750
|
+
app.router.add_get('/auth/github/callback', github_callback_handler)
|
|
1751
|
+
app.router.add_get('/auth/session', session_handler)
|
|
1752
|
+
app.router.add_post('/auth/logout', logout_handler)
|
|
1753
|
+
|
|
1507
1754
|
async def ui_static(request: web.Request) -> web.Response:
|
|
1508
1755
|
path = Path(request.match_info["path"])
|
|
1509
1756
|
|
|
@@ -1543,9 +1790,12 @@ def main():
|
|
|
1543
1790
|
enabled, disabled = provider_status()
|
|
1544
1791
|
ui['status'] = {
|
|
1545
1792
|
"all": list(g_config['providers'].keys()),
|
|
1546
|
-
"enabled": enabled,
|
|
1547
|
-
"disabled": disabled
|
|
1793
|
+
"enabled": enabled,
|
|
1794
|
+
"disabled": disabled
|
|
1548
1795
|
}
|
|
1796
|
+
# Add auth configuration
|
|
1797
|
+
ui['requiresAuth'] = auth_enabled
|
|
1798
|
+
ui['authType'] = 'oauth' if auth_enabled else 'apikey'
|
|
1549
1799
|
return web.json_response(ui)
|
|
1550
1800
|
app.router.add_get('/config', ui_config_handler)
|
|
1551
1801
|
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import { inject } from "vue"
|
|
1
2
|
import Sidebar from "./Sidebar.mjs"
|
|
2
3
|
|
|
3
4
|
export default {
|
|
4
5
|
components: {
|
|
5
6
|
Sidebar,
|
|
6
7
|
},
|
|
8
|
+
setup() {
|
|
9
|
+
const ai = inject('ai')
|
|
10
|
+
return { ai }
|
|
11
|
+
},
|
|
7
12
|
template: `
|
|
8
13
|
<div class="flex h-screen bg-white">
|
|
9
|
-
<!-- Sidebar -->
|
|
10
|
-
<div class="w-72 xl:w-80 flex-shrink-0">
|
|
14
|
+
<!-- Sidebar (hidden when auth required and not authenticated) -->
|
|
15
|
+
<div v-if="!(ai.requiresAuth && !ai.auth)" class="w-72 xl:w-80 flex-shrink-0">
|
|
11
16
|
<Sidebar />
|
|
12
17
|
</div>
|
|
13
18
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { computed, inject, ref, onMounted, onUnmounted } from "vue"
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
template:`
|
|
5
|
+
<div v-if="$ai.auth?.profileUrl" class="relative" ref="avatarContainer">
|
|
6
|
+
<img
|
|
7
|
+
@click.stop="toggleMenu"
|
|
8
|
+
:src="$ai.auth.profileUrl"
|
|
9
|
+
:title="authTitle"
|
|
10
|
+
class="size-8 rounded-full cursor-pointer hover:ring-2 hover:ring-gray-300"
|
|
11
|
+
/>
|
|
12
|
+
<div
|
|
13
|
+
v-if="showMenu"
|
|
14
|
+
@click.stop
|
|
15
|
+
class="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg py-1 z-50 border border-gray-200 dark:border-gray-700"
|
|
16
|
+
>
|
|
17
|
+
<div class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 border-b border-gray-200 dark:border-gray-700">
|
|
18
|
+
<div class="font-medium whitespace-nowrap overflow-hidden text-ellipsis">{{ $ai.auth.displayName || $ai.auth.userName }}</div>
|
|
19
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap overflow-hidden text-ellipsis">{{ $ai.auth.email }}</div>
|
|
20
|
+
</div>
|
|
21
|
+
<button type="button"
|
|
22
|
+
@click="handleLogout"
|
|
23
|
+
class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center whitespace-nowrap"
|
|
24
|
+
>
|
|
25
|
+
<svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
26
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
|
|
27
|
+
</svg>
|
|
28
|
+
Sign Out
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
`,
|
|
33
|
+
setup() {
|
|
34
|
+
const ai = inject('ai')
|
|
35
|
+
const showMenu = ref(false)
|
|
36
|
+
const avatarContainer = ref(null)
|
|
37
|
+
|
|
38
|
+
const authTitle = computed(() => {
|
|
39
|
+
if (!ai.auth) return ''
|
|
40
|
+
const { userId, userName, displayName, bearerToken, roles } = ai.auth
|
|
41
|
+
const name = userName || displayName
|
|
42
|
+
const prefix = roles && roles.includes('Admin') ? 'Admin' : 'Name'
|
|
43
|
+
const sb = [
|
|
44
|
+
name ? `${prefix}: ${name}` : '',
|
|
45
|
+
`API Key: ${bearerToken}`,
|
|
46
|
+
`${userId}`,
|
|
47
|
+
]
|
|
48
|
+
return sb.filter(x => x).join('\n')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
function toggleMenu() {
|
|
52
|
+
showMenu.value = !showMenu.value
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function handleLogout() {
|
|
56
|
+
showMenu.value = false
|
|
57
|
+
await ai.signOut()
|
|
58
|
+
// Reload the page to show sign-in screen
|
|
59
|
+
window.location.reload()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Close menu when clicking outside
|
|
63
|
+
const handleClickOutside = (event) => {
|
|
64
|
+
if (showMenu.value && avatarContainer.value && !avatarContainer.value.contains(event.target)) {
|
|
65
|
+
showMenu.value = false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onMounted(() => {
|
|
70
|
+
document.addEventListener('click', handleClickOutside)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
onUnmounted(() => {
|
|
74
|
+
document.removeEventListener('click', handleClickOutside)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
authTitle,
|
|
79
|
+
handleLogout,
|
|
80
|
+
showMenu,
|
|
81
|
+
toggleMenu,
|
|
82
|
+
avatarContainer,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|