Compare commits
9
Commits
f823e7d314
...
0.9.9.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70fd61608b | ||
|
|
07f6fec4bf | ||
|
|
6761a73364 | ||
|
|
0d1ce96c99 | ||
|
|
b16b3af44d | ||
|
|
fdbe0a74e8 | ||
|
|
0154c89c6b | ||
|
|
e3dbed9f5e | ||
|
|
5b37718120 |
No files matched your search
@@ -42,6 +42,8 @@ docker-compose.yml
|
|||||||
|
|
||||||
# Docs
|
# Docs
|
||||||
*.md
|
*.md
|
||||||
|
*.tex
|
||||||
|
*.pdf
|
||||||
|
|
||||||
# Dev & build scripts
|
# Dev & build scripts
|
||||||
run_dev.py
|
run_dev.py
|
||||||
|
|||||||
+5
-1
@@ -26,4 +26,8 @@ frontend/node_modules/
|
|||||||
frontend/dist/
|
frontend/dist/
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
config.json
|
config.json
|
||||||
|
|
||||||
|
# Generated documents
|
||||||
|
KURZANLEITUNG.tex
|
||||||
|
KURZANLEITUNG.pdf
|
||||||
@@ -6,6 +6,8 @@ COPY frontend/ frontend/
|
|||||||
RUN npm run build --prefix frontend
|
RUN npm run build --prefix frontend
|
||||||
|
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
ARG APP_VERSION=dev
|
||||||
|
ENV APP_VERSION=$APP_VERSION
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY backend/requirements.txt .
|
COPY backend/requirements.txt .
|
||||||
|
|||||||
+22
-16
@@ -7,14 +7,14 @@ Der Dienst stellt **große Sprachmodelle (LLMs)** über eine einfache HTTP-API b
|
|||||||
Typische Anwendungsfälle:
|
Typische Anwendungsfälle:
|
||||||
|
|
||||||
- Texte zusammenfassen, übersetzen oder umformulieren
|
- Texte zusammenfassen, übersetzen oder umformulieren
|
||||||
- KI-gestütztes Coding (z.B. mit **[opencode](https://opencode.ai)**)
|
- KI-gestütztes Coding (z.B. mit **[opencode](https://opencode.ai)**)
|
||||||
- Experimente mit Prompt-Engineering und LLM-Integration in eigene Projekte
|
- Experimente mit Prompt-Engineering und LLM-Integration in eigene Projekte
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Zugang
|
## Zugang
|
||||||
|
|
||||||
Der Dienst ist **nur im Intranet** erreichbar.
|
Der Dienst ist **nur im Intranet (VPN)** erreichbar.
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -69,7 +69,7 @@ print(response.choices[0].message.content)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Python-Beispiel – Modell wählen und auflisten
|
## Python-Beispiel – Verfügbare Modelle abfragen
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
@@ -79,25 +79,31 @@ BASE_URL = "http://141.75.33.244:8000/v1"
|
|||||||
|
|
||||||
client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
|
client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
|
||||||
|
|
||||||
# Verfügbare Modelle abrufen
|
|
||||||
models = client.models.list()
|
models = client.models.list()
|
||||||
for m in models.data:
|
for m in models.data:
|
||||||
print(m.id)
|
print(m.id)
|
||||||
|
|
||||||
# Prompt mit einem bestimmten Modell
|
|
||||||
response = client.chat.completions.create(
|
|
||||||
model="qwen3-coder-next:q8_0",
|
|
||||||
messages=[
|
|
||||||
{"role": "system", "content": "Du bist ein hilfreicher Coding-Assistent."},
|
|
||||||
{"role": "user", "content": "Schreibe eine Python-Funktion zum Berechnen der Fibonacci-Folge."}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
print(response.choices[0].message.content)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Aktuell geladenes Modell abfragen
|
||||||
|
|
||||||
|
Da immer nur ein Modell gleichzeitig im Speicher sein kann, lässt sich mit folgendem Aufruf prüfen, welches Modell gerade aktiv ist:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
r = httpx.get(
|
||||||
|
"http://141.75.33.244:8000/api/ps",
|
||||||
|
headers={"Authorization": "Bearer sk-..."}
|
||||||
|
)
|
||||||
|
print(r.json())
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Antwort enthält Modellname, Größe und wie lange das Modell noch im Speicher bleibt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Empfehlungen zur Nutzung
|
## Empfehlungen zur Nutzung
|
||||||
|
|
||||||
- **Kleines Modell zuerst** (`gemma4:31b` oder `gpt-oss:20b`) – viel schneller, für viele Aufgaben ausreichend.
|
- **Kleines Modell zuerst** (`gemma4:31b` oder `gpt-oss:20b`) – viel schneller, für viele Aufgaben ausreichend.
|
||||||
@@ -155,7 +161,7 @@ Für Code-Aufgaben empfiehlt sich `qwen3-coder-next:q8_0`, für allgemeine Aufga
|
|||||||
opencode
|
opencode
|
||||||
```
|
```
|
||||||
|
|
||||||
opencode öffnet eine interaktive TUI im Terminal und kann dann im Projektverzeichnis eingesetzt werden – Dateien lesen, Code generieren, Refactoring vorschlagen usw.
|
opencode öffnet eine interaktive Terminal-Oberfläche und kann dann im Projektverzeichnis eingesetzt werden – Dateien lesen, Code generieren, Refactoring vorschlagen usw.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -131,7 +131,10 @@ async def get_proxy_info(_ = Depends(require_admin_auth)):
|
|||||||
host = os.getenv("PROXY_HOST", "0.0.0.0")
|
host = os.getenv("PROXY_HOST", "0.0.0.0")
|
||||||
port = os.getenv("PROXY_PORT", "8000")
|
port = os.getenv("PROXY_PORT", "8000")
|
||||||
display_host = "localhost" if host in ("0.0.0.0", "::") else host
|
display_host = "localhost" if host in ("0.0.0.0", "::") else host
|
||||||
return {"endpoint": f"http://{display_host}:{port}"}
|
return {
|
||||||
|
"endpoint": f"http://{display_host}:{port}",
|
||||||
|
"version": os.getenv("APP_VERSION", "dev"),
|
||||||
|
}
|
||||||
|
|
||||||
@app.get("/api/settings", response_model=schemas.Settings)
|
@app.get("/api/settings", response_model=schemas.Settings)
|
||||||
async def read_settings(db: Session = Depends(get_db), _ = Depends(require_admin_auth)):
|
async def read_settings(db: Session = Depends(get_db), _ = Depends(require_admin_auth)):
|
||||||
|
|||||||
+45
-7
@@ -1,5 +1,7 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -102,12 +104,14 @@ async def generate(request: Request, db: Session = Depends(get_db)):
|
|||||||
prompt_preview = (body.get("prompt", "").replace("\n", " ").strip())[:120]
|
prompt_preview = (body.get("prompt", "").replace("\n", " ").strip())[:120]
|
||||||
usage_log.info('%s | /api/generate | %s | ~%d tokens | "%s"',
|
usage_log.info('%s | /api/generate | %s | ~%d tokens | "%s"',
|
||||||
request.state.api_key_name, body.get("model", "?"), prompt_tokens, prompt_preview)
|
request.state.api_key_name, body.get("model", "?"), prompt_tokens, prompt_preview)
|
||||||
|
start = time.monotonic()
|
||||||
try:
|
try:
|
||||||
response = await proxy_request(f"{ollama_url}/api/generate", method="POST", json_data=body)
|
response = await proxy_request(f"{ollama_url}/api/generate", method="POST", json_data=body)
|
||||||
resp_json = response.json()
|
resp_json = response.json()
|
||||||
usage_log.info('%s | /api/generate | %s | actual ↑%d ↓%d tokens',
|
usage_log.info('%s | /api/generate | %s | actual ↑%d ↓%d tokens | %.1fs',
|
||||||
request.state.api_key_name, body.get("model", "?"),
|
request.state.api_key_name, body.get("model", "?"),
|
||||||
resp_json.get("prompt_eval_count", 0), resp_json.get("eval_count", 0))
|
resp_json.get("prompt_eval_count", 0), resp_json.get("eval_count", 0),
|
||||||
|
time.monotonic() - start)
|
||||||
return JSONResponse(content=resp_json, status_code=response.status_code)
|
return JSONResponse(content=resp_json, status_code=response.status_code)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
error_log.error("Proxy error | %s | /api/generate | %s | %s: %s",
|
error_log.error("Proxy error | %s | /api/generate | %s | %s: %s",
|
||||||
@@ -131,12 +135,14 @@ async def chat(request: Request, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
usage_log.info('%s | /api/chat | %s | ~%d tokens | "%s"',
|
usage_log.info('%s | /api/chat | %s | ~%d tokens | "%s"',
|
||||||
request.state.api_key_name, body.get("model", "?"), prompt_tokens, _last_user_msg(messages))
|
request.state.api_key_name, body.get("model", "?"), prompt_tokens, _last_user_msg(messages))
|
||||||
|
start = time.monotonic()
|
||||||
try:
|
try:
|
||||||
response = await proxy_request(f"{ollama_url}/api/chat", method="POST", json_data=body)
|
response = await proxy_request(f"{ollama_url}/api/chat", method="POST", json_data=body)
|
||||||
resp_json = response.json()
|
resp_json = response.json()
|
||||||
usage_log.info('%s | /api/chat | %s | actual ↑%d ↓%d tokens',
|
usage_log.info('%s | /api/chat | %s | actual ↑%d ↓%d tokens | %.1fs',
|
||||||
request.state.api_key_name, body.get("model", "?"),
|
request.state.api_key_name, body.get("model", "?"),
|
||||||
resp_json.get("prompt_eval_count", 0), resp_json.get("eval_count", 0))
|
resp_json.get("prompt_eval_count", 0), resp_json.get("eval_count", 0),
|
||||||
|
time.monotonic() - start)
|
||||||
return JSONResponse(content=resp_json, status_code=response.status_code)
|
return JSONResponse(content=resp_json, status_code=response.status_code)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
error_log.error("Proxy error | %s | /api/chat | %s | %s: %s",
|
error_log.error("Proxy error | %s | /api/chat | %s | %s: %s",
|
||||||
@@ -149,6 +155,16 @@ async def list_models(db: Session = Depends(get_db)):
|
|||||||
response = await proxy_request(f"{ollama_url}/api/tags", method="GET")
|
response = await proxy_request(f"{ollama_url}/api/tags", method="GET")
|
||||||
return JSONResponse(content=response.json(), status_code=response.status_code)
|
return JSONResponse(content=response.json(), status_code=response.status_code)
|
||||||
|
|
||||||
|
@app.get("/version")
|
||||||
|
async def version():
|
||||||
|
return {"version": os.getenv("APP_VERSION", "dev")}
|
||||||
|
|
||||||
|
@app.get("/api/ps")
|
||||||
|
async def running_models(db: Session = Depends(get_db)):
|
||||||
|
ollama_url = crud.get_setting(db, "ollama_url", os.getenv("OLLAMA_URL", "http://localhost:11434"))
|
||||||
|
response = await proxy_request(f"{ollama_url}/api/ps", method="GET")
|
||||||
|
return JSONResponse(content=response.json(), status_code=response.status_code)
|
||||||
|
|
||||||
@app.get("/api/versions")
|
@app.get("/api/versions")
|
||||||
async def versions(db: Session = Depends(get_db)):
|
async def versions(db: Session = Depends(get_db)):
|
||||||
ollama_url = crud.get_setting(db, "ollama_url", os.getenv("OLLAMA_URL", "http://localhost:11434"))
|
ollama_url = crud.get_setting(db, "ollama_url", os.getenv("OLLAMA_URL", "http://localhost:11434"))
|
||||||
@@ -185,29 +201,51 @@ async def openai_chat_completions(request: Request, db: Session = Depends(get_db
|
|||||||
target = f"{ollama_url}/v1/chat/completions"
|
target = f"{ollama_url}/v1/chat/completions"
|
||||||
|
|
||||||
if body.get("stream"):
|
if body.get("stream"):
|
||||||
|
existing_opts = body.get("stream_options") or {}
|
||||||
|
stream_body = {**body, "stream_options": {**existing_opts, "include_usage": True}}
|
||||||
|
start = time.monotonic()
|
||||||
|
usage_tokens = {"prompt": 0, "completion": 0}
|
||||||
|
|
||||||
async def generate():
|
async def generate():
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=300.0) as client:
|
async with httpx.AsyncClient(timeout=300.0) as client:
|
||||||
async with client.stream("POST", target, json=body) as resp:
|
async with client.stream("POST", target, json=stream_body) as resp:
|
||||||
async for chunk in resp.aiter_bytes():
|
async for chunk in resp.aiter_bytes():
|
||||||
|
try:
|
||||||
|
for line in chunk.decode("utf-8", errors="ignore").splitlines():
|
||||||
|
if line.startswith("data: ") and "[DONE]" not in line:
|
||||||
|
data = json.loads(line[6:])
|
||||||
|
if u := data.get("usage"):
|
||||||
|
usage_tokens["prompt"] = u.get("prompt_tokens", 0)
|
||||||
|
usage_tokens["completion"] = u.get("completion_tokens", 0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
yield chunk
|
yield chunk
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
error_log.error("Stream error | %s | /v1/chat/completions | %s | %s: %s",
|
error_log.error("Stream error | %s | /v1/chat/completions | %s | %s: %s",
|
||||||
request.state.api_key_name, model_name, type(exc).__name__, exc, exc_info=exc)
|
request.state.api_key_name, model_name, type(exc).__name__, exc, exc_info=exc)
|
||||||
raise
|
raise
|
||||||
|
finally:
|
||||||
|
usage_log.info('%s | /v1/chat/completions | %s | actual ↑%d ↓%d tokens | %.1fs',
|
||||||
|
request.state.api_key_name, model_name,
|
||||||
|
usage_tokens["prompt"], usage_tokens["completion"],
|
||||||
|
time.monotonic() - start)
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
generate(),
|
generate(),
|
||||||
media_type="text/event-stream",
|
media_type="text/event-stream",
|
||||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
start = time.monotonic()
|
||||||
try:
|
try:
|
||||||
response = await proxy_request(target, method="POST", json_data=body)
|
response = await proxy_request(target, method="POST", json_data=body)
|
||||||
resp_json = response.json()
|
resp_json = response.json()
|
||||||
usage = resp_json.get("usage", {})
|
usage = resp_json.get("usage", {})
|
||||||
usage_log.info('%s | /v1/chat/completions | %s | actual ↑%d ↓%d tokens',
|
usage_log.info('%s | /v1/chat/completions | %s | actual ↑%d ↓%d tokens | %.1fs',
|
||||||
request.state.api_key_name, model_name,
|
request.state.api_key_name, model_name,
|
||||||
usage.get("prompt_tokens", 0), usage.get("completion_tokens", 0))
|
usage.get("prompt_tokens", 0), usage.get("completion_tokens", 0),
|
||||||
|
time.monotonic() - start)
|
||||||
return JSONResponse(content=resp_json, status_code=response.status_code)
|
return JSONResponse(content=resp_json, status_code=response.status_code)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
error_log.error("Proxy error | %s | /v1/chat/completions | %s | %s: %s",
|
error_log.error("Proxy error | %s | /v1/chat/completions | %s | %s: %s",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ echo ""
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform "$PLATFORM" \
|
--platform "$PLATFORM" \
|
||||||
--push \
|
--push \
|
||||||
|
--build-arg APP_VERSION="$VERSION" \
|
||||||
-t "$IMAGE:$VERSION" \
|
-t "$IMAGE:$VERSION" \
|
||||||
-t "$IMAGE:latest" \
|
-t "$IMAGE:latest" \
|
||||||
.
|
.
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ function SettingsSection({ password }) {
|
|||||||
const [modelsLoading, setModelsLoading] = useState(false);
|
const [modelsLoading, setModelsLoading] = useState(false);
|
||||||
const [ollamaReachable, setOllamaReachable] = useState(true);
|
const [ollamaReachable, setOllamaReachable] = useState(true);
|
||||||
const [proxyEndpoint, setProxyEndpoint] = useState(null);
|
const [proxyEndpoint, setProxyEndpoint] = useState(null);
|
||||||
|
const [appVersion, setAppVersion] = useState(null);
|
||||||
const [saved, setSaved] = useState(false);
|
const [saved, setSaved] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
@@ -115,6 +116,7 @@ function SettingsSection({ password }) {
|
|||||||
const s = settingsRes.data;
|
const s = settingsRes.data;
|
||||||
setSettings(s);
|
setSettings(s);
|
||||||
setProxyEndpoint(proxyRes.data.endpoint);
|
setProxyEndpoint(proxyRes.data.endpoint);
|
||||||
|
setAppVersion(proxyRes.data.version);
|
||||||
fetchModels(s.ollama_url, s.force_model);
|
fetchModels(s.ollama_url, s.force_model);
|
||||||
}).catch(() => setError('Einstellungen konnten nicht geladen werden.'));
|
}).catch(() => setError('Einstellungen konnten nicht geladen werden.'));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -145,6 +147,10 @@ function SettingsSection({ password }) {
|
|||||||
<small> (Änderung erfordert Neustart)</small>
|
<small> (Änderung erfordert Neustart)</small>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="settings-row">
|
||||||
|
<label>Version</label>
|
||||||
|
<span className="settings-value">{appVersion ?? '…'}</span>
|
||||||
|
</div>
|
||||||
<div className="settings-row">
|
<div className="settings-row">
|
||||||
<label>Ollama-Endpunkt</label>
|
<label>Ollama-Endpunkt</label>
|
||||||
<div className="settings-input-wrap">
|
<div className="settings-input-wrap">
|
||||||
|
|||||||
Reference in new issue
Block a user