Skip to content

Commit c8c6540

Browse files
committed
intitial docker impl
1 parent fc9727c commit c8c6540

File tree

7 files changed

+167
-4
lines changed

7 files changed

+167
-4
lines changed

.env.example

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Port
2+
PORT=8000
3+
4+
# Auth dir
5+
CHATGPT_LOCAL_HOME=/data
6+
7+
# show request/stream logs
8+
VERBOSE=false
9+
10+
# OAuth client id (modify only if you know what you're doing)
11+
# CHATGPT_LOCAL_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
12+
13+
# Reasoning controls
14+
CHATGPT_LOCAL_REASONING_EFFORT=medium # minimal|low|medium|high
15+
CHATGPT_LOCAL_REASONING_SUMMARY=auto # auto|concise|detailed|none
16+
CHATGPT_LOCAL_REASONING_COMPAT=think-tags # legacy|o3|think-tags|current
17+
18+
# Force a specific model name
19+
# CHATGPT_LOCAL_DEBUG_MODEL=gpt-5

DOCKER.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Docker Deployment
2+
3+
## Quick Start
4+
1) Setup env:
5+
cp .env.example .env
6+
7+
2) Build the image:
8+
docker compose build
9+
10+
3) Login:
11+
docker compose run --rm --service-ports chatmock-login login
12+
- The command prints an auth URL, copy paste it into your browser.
13+
- Server should stop automatically once it recieves the tokens and they are saved.
14+
15+
4) Start the server:
16+
docker compose up -d chatmock
17+
18+
5) Free to use it in whichever chat app you like!
19+
20+
## Configuration
21+
Set options in `.env` or pass environment variables:
22+
- `PORT`: Container listening port (default 8000)
23+
- `VERBOSE`: `true|false` to enable request/stream logs
24+
- `CHATGPT_LOCAL_REASONING_EFFORT`: minimal|low|medium|high
25+
- `CHATGPT_LOCAL_REASONING_SUMMARY`: auto|concise|detailed|none
26+
- `CHATGPT_LOCAL_REASONING_COMPAT`: legacy|o3|think-tags|current
27+
- `CHATGPT_LOCAL_DEBUG_MODEL`: force model override (e.g., `gpt-5`)
28+
- `CHATGPT_LOCAL_CLIENT_ID`: OAuth client id override (rarely needed)
29+
30+
## Logs
31+
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.
32+
33+
## Test
34+
35+
```
36+
curl -s http://localhost:8000/v1/chat/completions \
37+
-H 'Content-Type: application/json' \
38+
-d '{"model":"gpt-5","messages":[{"role":"user","content":"Hello world!"}]}' | jq .
39+
```

Dockerfile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
FROM python:3.11-slim
2+
3+
ENV PYTHONDONTWRITEBYTECODE=1 \
4+
PYTHONUNBUFFERED=1
5+
6+
WORKDIR /app
7+
8+
COPY requirements.txt ./
9+
RUN pip install --no-cache-dir -r requirements.txt
10+
11+
COPY . /app
12+
13+
RUN mkdir -p /data
14+
15+
COPY docker/entrypoint.sh /entrypoint.sh
16+
RUN chmod +x /entrypoint.sh
17+
18+
EXPOSE 8000 1455
19+
20+
ENTRYPOINT ["/entrypoint.sh"]
21+
CMD ["serve"]
22+

chatmock/cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .config import CLIENT_ID_DEFAULT
1212
from .oauth import OAuthHTTPServer, OAuthHandler, REQUIRED_PORT, URL_BASE
1313
from .utils import eprint, get_home_dir, load_chatgpt_tokens, parse_jwt_claims, read_auth_file
14+
import os
1415

1516

1617
def cmd_login(no_browser: bool, verbose: bool) -> int:
@@ -21,7 +22,8 @@ def cmd_login(no_browser: bool, verbose: bool) -> int:
2122
return 1
2223

2324
try:
24-
httpd = OAuthHTTPServer(("127.0.0.1", REQUIRED_PORT), OAuthHandler, home_dir=home_dir, client_id=client_id, verbose=verbose)
25+
bind_host = os.getenv("CHATGPT_LOCAL_LOGIN_BIND", "127.0.0.1")
26+
httpd = OAuthHTTPServer((bind_host, REQUIRED_PORT), OAuthHandler, home_dir=home_dir, client_id=client_id, verbose=verbose)
2527
except OSError as e:
2628
eprint(f"ERROR: {e}")
2729
if e.errno == errno.EADDRINUSE:

chatmock/oauth.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import datetime
4+
import ssl
45
import http.server
56
import json
67
import secrets
@@ -10,6 +11,8 @@
1011
import urllib.request
1112
from typing import Any, Dict, Tuple
1213

14+
import certifi
15+
1316
from .models import AuthBundle, PkceCodes, TokenData
1417
from .utils import eprint, generate_pkce, parse_jwt_claims, write_auth_file
1518

@@ -34,6 +37,7 @@
3437
</html>
3538
"""
3639

40+
_SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
3741

3842
class OAuthHTTPServer(http.server.HTTPServer):
3943
def __init__(
@@ -174,7 +178,8 @@ def _exchange_code(self, code: str) -> Tuple[AuthBundle, str]:
174178
data=data,
175179
method="POST",
176180
headers={"Content-Type": "application/x-www-form-urlencoded"},
177-
)
181+
),
182+
context=_SSL_CONTEXT,
178183
) as resp:
179184
payload = json.loads(resp.read().decode())
180185

@@ -242,7 +247,8 @@ def _maybe_obtain_api_key(
242247
data=exchange_data,
243248
method="POST",
244249
headers={"Content-Type": "application/x-www-form-urlencoded"},
245-
)
250+
),
251+
context=_SSL_CONTEXT,
246252
) as resp:
247253
exchange_payload = json.loads(resp.read().decode())
248254
exchanged_access_token = exchange_payload.get("access_token")
@@ -258,4 +264,3 @@ def _maybe_obtain_api_key(
258264
}
259265
success_url = f"{URL_BASE}/success?{urllib.parse.urlencode(success_url_query)}"
260266
return exchanged_access_token, success_url
261-

docker-compose.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
version: "3.9"
2+
3+
services:
4+
chatmock:
5+
build: .
6+
image: chatmock:latest
7+
container_name: chatmock
8+
command: ["serve"]
9+
env_file: .env
10+
environment:
11+
- CHATGPT_LOCAL_HOME=/data
12+
ports:
13+
- "8000:8000"
14+
volumes:
15+
- chatmock_data:/data
16+
- ./prompt.md:/app/prompt.md:ro
17+
healthcheck:
18+
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)\" "]
19+
interval: 10s
20+
timeout: 5s
21+
retries: 5
22+
start_period: 5s
23+
24+
chatmock-login:
25+
image: chatmock:latest
26+
profiles: ["login"]
27+
command: ["login"]
28+
environment:
29+
- CHATGPT_LOCAL_HOME=/data
30+
- CHATGPT_LOCAL_LOGIN_BIND=0.0.0.0
31+
volumes:
32+
- chatmock_data:/data
33+
ports:
34+
- "1455:1455"
35+
36+
volumes:
37+
chatmock_data:

docker/entrypoint.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
export CHATGPT_LOCAL_HOME="${CHATGPT_LOCAL_HOME:-/data}"
5+
6+
cmd="${1:-serve}"
7+
shift || true
8+
9+
bool() {
10+
case "${1:-}" in
11+
1|true|TRUE|yes|YES|on|ON) return 0;;
12+
*) return 1;;
13+
esac
14+
}
15+
16+
if [[ "$cmd" == "serve" ]]; then
17+
PORT="${PORT:-8000}"
18+
ARGS=(serve --host 0.0.0.0 --port "${PORT}")
19+
20+
if bool "${VERBOSE:-}" || bool "${CHATGPT_LOCAL_VERBOSE:-}"; then
21+
ARGS+=(--verbose)
22+
fi
23+
24+
if [[ "$#" -gt 0 ]]; then
25+
ARGS+=("$@")
26+
fi
27+
28+
exec python chatmock.py "${ARGS[@]}"
29+
elif [[ "$cmd" == "login" ]]; then
30+
ARGS=(login --no-browser)
31+
if bool "${VERBOSE:-}" || bool "${CHATGPT_LOCAL_VERBOSE:-}"; then
32+
ARGS+=(--verbose)
33+
fi
34+
35+
exec python chatmock.py "${ARGS[@]}"
36+
else
37+
exec "$cmd" "$@"
38+
fi
39+

0 commit comments

Comments
 (0)