intitial docker impl

This commit is contained in:
Game_Time
2025-08-20 15:26:14 +05:00
parent fc9727cb73
commit c8c6540d23
7 changed files with 167 additions and 4 deletions

19
.env.example Normal file
View 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
View 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
View 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"]

View File

@@ -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:

View File

@@ -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
View 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
View 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