intitial docker impl
This commit is contained in:
19
.env.example
Normal file
19
.env.example
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Port
|
||||||
|
PORT=8000
|
||||||
|
|
||||||
|
# Auth dir
|
||||||
|
CHATGPT_LOCAL_HOME=/data
|
||||||
|
|
||||||
|
# show request/stream logs
|
||||||
|
VERBOSE=false
|
||||||
|
|
||||||
|
# OAuth client id (modify only if you know what you're doing)
|
||||||
|
# CHATGPT_LOCAL_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
|
||||||
|
|
||||||
|
# Reasoning controls
|
||||||
|
CHATGPT_LOCAL_REASONING_EFFORT=medium # minimal|low|medium|high
|
||||||
|
CHATGPT_LOCAL_REASONING_SUMMARY=auto # auto|concise|detailed|none
|
||||||
|
CHATGPT_LOCAL_REASONING_COMPAT=think-tags # legacy|o3|think-tags|current
|
||||||
|
|
||||||
|
# Force a specific model name
|
||||||
|
# CHATGPT_LOCAL_DEBUG_MODEL=gpt-5
|
||||||
39
DOCKER.md
Normal file
39
DOCKER.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Docker Deployment
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
1) Setup env:
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
2) Build the image:
|
||||||
|
docker compose build
|
||||||
|
|
||||||
|
3) Login:
|
||||||
|
docker compose run --rm --service-ports chatmock-login login
|
||||||
|
- The command prints an auth URL, copy paste it into your browser.
|
||||||
|
- Server should stop automatically once it recieves the tokens and they are saved.
|
||||||
|
|
||||||
|
4) Start the server:
|
||||||
|
docker compose up -d chatmock
|
||||||
|
|
||||||
|
5) Free to use it in whichever chat app you like!
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
Set options in `.env` or pass environment variables:
|
||||||
|
- `PORT`: Container listening port (default 8000)
|
||||||
|
- `VERBOSE`: `true|false` to enable request/stream logs
|
||||||
|
- `CHATGPT_LOCAL_REASONING_EFFORT`: minimal|low|medium|high
|
||||||
|
- `CHATGPT_LOCAL_REASONING_SUMMARY`: auto|concise|detailed|none
|
||||||
|
- `CHATGPT_LOCAL_REASONING_COMPAT`: legacy|o3|think-tags|current
|
||||||
|
- `CHATGPT_LOCAL_DEBUG_MODEL`: force model override (e.g., `gpt-5`)
|
||||||
|
- `CHATGPT_LOCAL_CLIENT_ID`: OAuth client id override (rarely needed)
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
Set `VERBOSE=true` to include extra logging for debugging issues in upstream or chat app requests. Please include and use these logs when submitting bug reports.
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -s http://localhost:8000/v1/chat/completions \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"model":"gpt-5","messages":[{"role":"user","content":"Hello world!"}]}' | jq .
|
||||||
|
```
|
||||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
RUN mkdir -p /data
|
||||||
|
|
||||||
|
COPY docker/entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 8000 1455
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
CMD ["serve"]
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ from .app import create_app
|
|||||||
from .config import CLIENT_ID_DEFAULT
|
from .config import CLIENT_ID_DEFAULT
|
||||||
from .oauth import OAuthHTTPServer, OAuthHandler, REQUIRED_PORT, URL_BASE
|
from .oauth import OAuthHTTPServer, OAuthHandler, REQUIRED_PORT, URL_BASE
|
||||||
from .utils import eprint, get_home_dir, load_chatgpt_tokens, parse_jwt_claims, read_auth_file
|
from .utils import eprint, get_home_dir, load_chatgpt_tokens, parse_jwt_claims, read_auth_file
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
def cmd_login(no_browser: bool, verbose: bool) -> int:
|
def cmd_login(no_browser: bool, verbose: bool) -> int:
|
||||||
@@ -21,7 +22,8 @@ def cmd_login(no_browser: bool, verbose: bool) -> int:
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
httpd = OAuthHTTPServer(("127.0.0.1", REQUIRED_PORT), OAuthHandler, home_dir=home_dir, client_id=client_id, verbose=verbose)
|
bind_host = os.getenv("CHATGPT_LOCAL_LOGIN_BIND", "127.0.0.1")
|
||||||
|
httpd = OAuthHTTPServer((bind_host, REQUIRED_PORT), OAuthHandler, home_dir=home_dir, client_id=client_id, verbose=verbose)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
eprint(f"ERROR: {e}")
|
eprint(f"ERROR: {e}")
|
||||||
if e.errno == errno.EADDRINUSE:
|
if e.errno == errno.EADDRINUSE:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import ssl
|
||||||
import http.server
|
import http.server
|
||||||
import json
|
import json
|
||||||
import secrets
|
import secrets
|
||||||
@@ -10,6 +11,8 @@ import urllib.parse
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
from typing import Any, Dict, Tuple
|
from typing import Any, Dict, Tuple
|
||||||
|
|
||||||
|
import certifi
|
||||||
|
|
||||||
from .models import AuthBundle, PkceCodes, TokenData
|
from .models import AuthBundle, PkceCodes, TokenData
|
||||||
from .utils import eprint, generate_pkce, parse_jwt_claims, write_auth_file
|
from .utils import eprint, generate_pkce, parse_jwt_claims, write_auth_file
|
||||||
|
|
||||||
@@ -34,6 +37,7 @@ LOGIN_SUCCESS_HTML = """<!DOCTYPE html>
|
|||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
|
||||||
|
|
||||||
class OAuthHTTPServer(http.server.HTTPServer):
|
class OAuthHTTPServer(http.server.HTTPServer):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -174,7 +178,8 @@ class OAuthHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
data=data,
|
data=data,
|
||||||
method="POST",
|
method="POST",
|
||||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
)
|
),
|
||||||
|
context=_SSL_CONTEXT,
|
||||||
) as resp:
|
) as resp:
|
||||||
payload = json.loads(resp.read().decode())
|
payload = json.loads(resp.read().decode())
|
||||||
|
|
||||||
@@ -242,7 +247,8 @@ class OAuthHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
data=exchange_data,
|
data=exchange_data,
|
||||||
method="POST",
|
method="POST",
|
||||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
)
|
),
|
||||||
|
context=_SSL_CONTEXT,
|
||||||
) as resp:
|
) as resp:
|
||||||
exchange_payload = json.loads(resp.read().decode())
|
exchange_payload = json.loads(resp.read().decode())
|
||||||
exchanged_access_token = exchange_payload.get("access_token")
|
exchanged_access_token = exchange_payload.get("access_token")
|
||||||
@@ -258,4 +264,3 @@ class OAuthHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
}
|
}
|
||||||
success_url = f"{URL_BASE}/success?{urllib.parse.urlencode(success_url_query)}"
|
success_url = f"{URL_BASE}/success?{urllib.parse.urlencode(success_url_query)}"
|
||||||
return exchanged_access_token, success_url
|
return exchanged_access_token, success_url
|
||||||
|
|
||||||
|
|||||||
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
chatmock:
|
||||||
|
build: .
|
||||||
|
image: chatmock:latest
|
||||||
|
container_name: chatmock
|
||||||
|
command: ["serve"]
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
- CHATGPT_LOCAL_HOME=/data
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- chatmock_data:/data
|
||||||
|
- ./prompt.md:/app/prompt.md:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "python -c \"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health').status==200 else 1)\" "]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 5s
|
||||||
|
|
||||||
|
chatmock-login:
|
||||||
|
image: chatmock:latest
|
||||||
|
profiles: ["login"]
|
||||||
|
command: ["login"]
|
||||||
|
environment:
|
||||||
|
- CHATGPT_LOCAL_HOME=/data
|
||||||
|
- CHATGPT_LOCAL_LOGIN_BIND=0.0.0.0
|
||||||
|
volumes:
|
||||||
|
- chatmock_data:/data
|
||||||
|
ports:
|
||||||
|
- "1455:1455"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
chatmock_data:
|
||||||
39
docker/entrypoint.sh
Normal file
39
docker/entrypoint.sh
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
export CHATGPT_LOCAL_HOME="${CHATGPT_LOCAL_HOME:-/data}"
|
||||||
|
|
||||||
|
cmd="${1:-serve}"
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
bool() {
|
||||||
|
case "${1:-}" in
|
||||||
|
1|true|TRUE|yes|YES|on|ON) return 0;;
|
||||||
|
*) return 1;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$cmd" == "serve" ]]; then
|
||||||
|
PORT="${PORT:-8000}"
|
||||||
|
ARGS=(serve --host 0.0.0.0 --port "${PORT}")
|
||||||
|
|
||||||
|
if bool "${VERBOSE:-}" || bool "${CHATGPT_LOCAL_VERBOSE:-}"; then
|
||||||
|
ARGS+=(--verbose)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$#" -gt 0 ]]; then
|
||||||
|
ARGS+=("$@")
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec python chatmock.py "${ARGS[@]}"
|
||||||
|
elif [[ "$cmd" == "login" ]]; then
|
||||||
|
ARGS=(login --no-browser)
|
||||||
|
if bool "${VERBOSE:-}" || bool "${CHATGPT_LOCAL_VERBOSE:-}"; then
|
||||||
|
ARGS+=(--verbose)
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec python chatmock.py "${ARGS[@]}"
|
||||||
|
else
|
||||||
|
exec "$cmd" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
Reference in New Issue
Block a user