suzmen commited on
Commit
2af6ef5
·
verified ·
1 Parent(s): dc2044e

Upload 64 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +25 -0
  2. .env +7 -0
  3. .env.example +7 -0
  4. .gitattributes +2 -0
  5. .gitignore +16 -0
  6. .pytest_cache/.gitignore +2 -0
  7. .pytest_cache/CACHEDIR.TAG +4 -0
  8. .pytest_cache/README.md +8 -0
  9. .pytest_cache/v/cache/lastfailed +1 -0
  10. .pytest_cache/v/cache/nodeids +4 -0
  11. Dockerfile +26 -0
  12. LICENSE +21 -0
  13. README.md +235 -10
  14. app/__init__.py +3 -0
  15. app/__pycache__/__init__.cpython-314.pyc +0 -0
  16. app/__pycache__/config.cpython-314.pyc +0 -0
  17. app/__pycache__/dependencies.cpython-314.pyc +0 -0
  18. app/__pycache__/main.cpython-314.pyc +0 -0
  19. app/api/__init__.py +1 -0
  20. app/api/__pycache__/__init__.cpython-314.pyc +0 -0
  21. app/api/__pycache__/custom.cpython-314.pyc +0 -0
  22. app/api/custom.py +31 -0
  23. app/api/v1/__init__.py +1 -0
  24. app/api/v1/__pycache__/__init__.cpython-314.pyc +0 -0
  25. app/api/v1/__pycache__/chat.cpython-314.pyc +0 -0
  26. app/api/v1/__pycache__/models.cpython-314.pyc +0 -0
  27. app/api/v1/__pycache__/responses.cpython-314.pyc +0 -0
  28. app/api/v1/__pycache__/router.cpython-314.pyc +0 -0
  29. app/api/v1/chat.py +16 -0
  30. app/api/v1/models.py +17 -0
  31. app/api/v1/responses.py +31 -0
  32. app/api/v1/router.py +11 -0
  33. app/config.py +27 -0
  34. app/dependencies.py +23 -0
  35. app/main.py +56 -0
  36. app/schemas/__init__.py +1 -0
  37. app/schemas/__pycache__/__init__.cpython-314.pyc +0 -0
  38. app/schemas/__pycache__/chat.cpython-314.pyc +0 -0
  39. app/schemas/__pycache__/responses.cpython-314.pyc +0 -0
  40. app/schemas/chat.py +28 -0
  41. app/schemas/responses.py +15 -0
  42. app/services/__init__.py +1 -0
  43. app/services/__pycache__/__init__.cpython-314.pyc +0 -0
  44. app/services/__pycache__/browser.cpython-314.pyc +0 -0
  45. app/services/__pycache__/chat.cpython-314.pyc +0 -0
  46. app/services/browser.py +143 -0
  47. app/services/chat.py +108 -0
  48. app/utils/__init__.py +1 -0
  49. app/utils/__pycache__/__init__.cpython-314.pyc +0 -0
  50. app/utils/__pycache__/parser.cpython-314.pyc +0 -0
.dockerignore ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Git
2
+ .git
3
+ .gitignore
4
+
5
+ # Environment
6
+ .env
7
+ .env.example
8
+
9
+ # Python
10
+ __pycache__/
11
+ *.py[cod]
12
+ *$py.class
13
+ .pytest_cache/
14
+ .venv/
15
+ venv/
16
+
17
+ # Documentation / Media
18
+ demo.mp4
19
+ gui_screenshot.png
20
+ n8n_preview.png
21
+
22
+ # Docker
23
+ docker-compose.yml
24
+ Dockerfile
25
+ .dockerignore
.env ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # PhantomAPI Configuration
2
+ # Copy this file to .env and adjust values
3
+ API_SECRET_KEY=change-me-to-a-strong-secret
4
+ HOST=0.0.0.0
5
+ PORT=7777
6
+ HEADLESS=true
7
+ BROWSER_TIMEOUT=120000
.env.example ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # PhantomAPI Configuration
2
+ # Copy this file to .env and adjust values
3
+ API_SECRET_KEY=change-me-to-a-strong-secret
4
+ HOST=0.0.0.0
5
+ PORT=7777
6
+ HEADLESS=true
7
+ BROWSER_TIMEOUT=120000
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ demo.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ n8n_preview.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ venv/
9
+ .venv/
10
+ .env
11
+ .vscode/
12
+ .idea/
13
+ .DS_Store
14
+ Thumbs.db
15
+ *.log
16
+ playwright-report/
.pytest_cache/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Created by pytest automatically.
2
+ *
.pytest_cache/CACHEDIR.TAG ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag created by pytest.
3
+ # For information about cache directory tags, see:
4
+ # https://bford.info/cachedir/spec.html
.pytest_cache/README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # pytest cache directory #
2
+
3
+ This directory contains data from the pytest's cache plugin,
4
+ which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
5
+
6
+ **Do not** commit this to version control.
7
+
8
+ See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
.pytest_cache/v/cache/lastfailed ADDED
@@ -0,0 +1 @@
 
 
1
+ {}
.pytest_cache/v/cache/nodeids ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ [
2
+ "tests/test_custom.py::test_custom_api_endpoint[asyncio]",
3
+ "tests/test_custom.py::test_custom_api_error_handling[asyncio]"
4
+ ]
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Install system dependencies for Playwright/Chromium
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ wget gnupg fonts-liberation libnss3 libatk-bridge2.0-0 libdrm2 \
6
+ libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 \
7
+ libasound2 libpangocairo-1.0-0 libgtk-3-0 \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Set up a new user named "user" with user ID 1000
11
+ RUN useradd -m -u 1000 user
12
+ USER user
13
+ ENV HOME=/home/user \
14
+ PATH=/home/user/.local/bin:$PATH
15
+
16
+ WORKDIR $HOME/app
17
+
18
+ COPY --chown=user requirements.txt .
19
+ RUN pip install --no-cache-dir --user -r requirements.txt \
20
+ && python -m playwright install chromium
21
+
22
+ COPY --chown=user . .
23
+
24
+ EXPOSE 7860
25
+
26
+ CMD ["python", "run.py"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mrshibly
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,10 +1,235 @@
1
- ---
2
- title: PutuAPI
3
- emoji: 🐨
4
- colorFrom: purple
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: PhantomAPI
3
+ emoji: 👻
4
+ colorFrom: purple
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ <div align="center">
11
+
12
+ # 👻 PhantomAPI
13
+
14
+ ### Turn ChatGPT into a FREE OpenAI-Compatible API
15
+
16
+ [![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=for-the-badge&logo=fastapi)](https://fastapi.tiangolo.com/)
17
+ [![Playwright](https://img.shields.io/badge/Playwright-2EAD33?style=for-the-badge&logo=playwright&logoColor=white)](https://playwright.dev/)
18
+ [![Docker](https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white)](https://docker.com/)
19
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](LICENSE)
20
+
21
+ **The invisible proxy that bridges ChatGPT's free web interface with your AI automation workflows.**
22
+
23
+ [Quick Start](#-quick-start) · [n8n Integration](#-connecting-to-n8n) · [Architecture](#-architecture) · [Docker](#-docker-deployment)
24
+
25
+ </div>
26
+
27
+ <div align="center">
28
+ <video src="demo.mp4" width="800" controls muted autoplay loop></video>
29
+ </div>
30
+
31
+ ---
32
+
33
+ ## 🌟 What is PhantomAPI?
34
+
35
+ **PhantomAPI** is a high-performance proxy server that makes ChatGPT's free web interface behave like the official OpenAI API. It's designed as a **drop-in replacement** for any tool that speaks the OpenAI protocol — especially **n8n**.
36
+
37
+ ### ✨ Key Features
38
+
39
+ | Feature | Description |
40
+ |:---|:---|
41
+ | 💸 **Zero API Costs** | Uses ChatGPT's free web interface via headless browser automation |
42
+ | ⚡ **Async Architecture** | Built on FastAPI with a dedicated browser thread for non-blocking requests |
43
+ | 🤖 **AI Agent Support** | Full tool-calling / function-calling support for n8n Agent nodes |
44
+ | 🔒 **API Key Auth** | Protected with Bearer token authentication |
45
+ | 🐳 **Docker Ready** | Deploy in seconds with `docker-compose up` |
46
+ | 🎨 **Built-in GUI** | A sleek dark-mode chat interface for quick testing |
47
+ | 📐 **Clean Architecture** | Proper FastAPI structure — routers, schemas, services, utils |
48
+
49
+ ---
50
+
51
+ ## ⚙️ How It Works
52
+
53
+ ```
54
+ ┌──────────┐ HTTP/JSON ┌──────────────┐ Playwright ┌──────────────┐
55
+ │ n8n │ ──────────────────▶ │ PhantomAPI │ ──────────────────▶ │ ChatGPT │
56
+ │ (or any │ ◀────────────────── │ (FastAPI) │ ◀────────────────── │ (Web UI) │
57
+ │ client) │ OpenAI Schema │ │ Scrape Response │ │
58
+ └──────────┘ └──────────────┘ └──────────────┘
59
+ ```
60
+
61
+ 1. **You send** a standard OpenAI API request to PhantomAPI
62
+ 2. **PhantomAPI** formats your messages into a prompt and types it into ChatGPT's web interface using a stealth browser
63
+ 3. **ChatGPT responds** on the web page — PhantomAPI scrapes the text
64
+ 4. **The response** is formatted back into the official OpenAI JSON schema and returned to you
65
+
66
+ ---
67
+
68
+ ## 🛠️ Quick Start
69
+
70
+ ### Prerequisites
71
+ - **Python 3.10+**
72
+ - **Google Chrome** installed on your system
73
+
74
+ ### 1. Clone & Install
75
+
76
+ ```bash
77
+ git clone https://github.com/mrshibly/phantom-api.git
78
+ cd phantom-api
79
+ pip install -r requirements.txt
80
+ python -m playwright install chromium
81
+ ```
82
+
83
+ ### 2. Configure
84
+
85
+ ```bash
86
+ cp .env.example .env
87
+ # Edit .env and set your API_SECRET_KEY
88
+ ```
89
+
90
+ ### 3. Run
91
+
92
+ ```bash
93
+ python run.py
94
+ ```
95
+
96
+ The server will start on `http://localhost:7777`.
97
+
98
+ | Endpoint | Description |
99
+ |:---|:---|
100
+ | `http://localhost:7777/` | Health check |
101
+ | `http://localhost:7777/docs` | Swagger UI (interactive API docs) |
102
+ | `http://localhost:7777/gui` | Chat GUI for quick testing |
103
+
104
+ ---
105
+
106
+ ## 🔌 Connecting to n8n
107
+
108
+ <div align="center">
109
+ <img src="n8n_preview.png" width="800" alt="n8n Workflow Example">
110
+ </div>
111
+
112
+ ### Method 1: OpenAI Node (Recommended)
113
+
114
+ 1. In n8n, go to **Credentials → New → OpenAI API**
115
+ 2. Set **Base URL** to: `http://127.0.0.1:7777/v1`
116
+ 3. Set **API Key** to your `API_SECRET_KEY` from `.env`
117
+ 4. Use this credential in any **OpenAI** or **AI Agent** node
118
+
119
+ > **Docker Tip:** If n8n runs in Docker, use `http://host.docker.internal:7777/v1`
120
+
121
+ ### Method 2: HTTP Request Node
122
+
123
+ 1. Add an **HTTP Request** node
124
+ 2. **Method:** `POST`
125
+ 3. **URL:** `http://127.0.0.1:7777/v1/chat/completions`
126
+ 4. **Authentication:** Header Auth → `Authorization: Bearer YOUR_KEY`
127
+ 5. **Body (JSON):**
128
+
129
+ ```json
130
+ {
131
+ "model": "gpt-4o-mini",
132
+ "messages": [
133
+ { "role": "user", "content": "Hello, PhantomAPI!" }
134
+ ]
135
+ }
136
+ ```
137
+
138
+ ---
139
+
140
+ ## 📐 Architecture
141
+
142
+ ```
143
+ phantom-api/
144
+ ├── app/
145
+ │ ├── main.py # App factory, CORS, lifespan
146
+ │ ├── config.py # Environment-driven settings
147
+ │ ├── dependencies.py # Auth dependency injection
148
+ │ ├── api/v1/
149
+ │ │ ├── router.py # Route aggregator
150
+ │ │ ├── chat.py # POST /v1/chat/completions
151
+ │ │ ├── responses.py # POST /v1/responses
152
+ │ │ └── models.py # GET /v1/models
153
+ │ ├── schemas/
154
+ │ │ ├── chat.py # Request/Response models
155
+ │ │ └── responses.py # Responses API models
156
+ │ ├── services/
157
+ │ │ └── browser.py # Playwright browser engine
158
+ │ └── utils/
159
+ │ ├── prompt.py # Smart prompt builder
160
+ │ └── parser.py # Tool-call JSON parser
161
+ ├── static/
162
+ │ └── index.html # Chat GUI
163
+ ├── tests/
164
+ │ └── test_health.py # Endpoint tests
165
+ ├── Dockerfile
166
+ ├── docker-compose.yml
167
+ ├── requirements.txt
168
+ ├── .env.example
169
+ └── run.py # Entry point
170
+ ```
171
+
172
+ ---
173
+
174
+ ## 🐳 Docker Deployment
175
+
176
+ ```bash
177
+ # Build and run
178
+ docker-compose up --build -d
179
+
180
+ # The server is now running on http://localhost:7777
181
+ ```
182
+
183
+ ---
184
+
185
+ ## 🤗 Hugging Face Spaces Deployment
186
+
187
+ You can deploy PhantomAPI directly to Hugging Face Spaces for free.
188
+
189
+ 1. **Create a New Space**: Go to [huggingface.co/new-space](https://huggingface.co/new-space)
190
+ 2. **Select SDK**: Choose **Docker**
191
+ 3. **Choose Template**: Select **Blank** (The existing `Dockerfile` will be used automatically)
192
+ 4. **Configure Secrets**:
193
+ - Go to **Settings → Variables and secrets** in your Space
194
+ - Add a new **Secret** named `API_SECRET_KEY` with your desired token
195
+ 5. **Push Code**:
196
+ ```bash
197
+ git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
198
+ git push -u hf main
199
+ ```
200
+
201
+ PhantomAPI will automatically start on port `7860`.
202
+
203
+ ---
204
+
205
+ ## 🔧 API Reference
206
+
207
+ ### `POST /v1/chat/completions`
208
+
209
+ Standard OpenAI Chat Completions endpoint. Supports messages, tools, and function calling.
210
+
211
+ ### `POST /v1/responses`
212
+
213
+ Modern Responses API for newer n8n versions. Accepts `input` (string or messages) and optional `instructions`.
214
+
215
+ ### `GET /v1/models`
216
+
217
+ Returns available model identifiers (used by n8n's model dropdown).
218
+
219
+ ### `GET /`
220
+
221
+ Health check — returns server status and version.
222
+
223
+ ---
224
+
225
+ ## 📄 License
226
+
227
+ This project is open-sourced under the [MIT License](LICENSE).
228
+
229
+ ---
230
+
231
+ <div align="center">
232
+
233
+ **Built with ❤️ by [mrshibly](https://github.com/mrshibly)**
234
+
235
+ </div>
app/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """PhantomAPI — Application package."""
2
+
3
+ __version__ = "1.0.0"
app/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (245 Bytes). View file
 
app/__pycache__/config.cpython-314.pyc ADDED
Binary file (1.29 kB). View file
 
app/__pycache__/dependencies.cpython-314.pyc ADDED
Binary file (1.28 kB). View file
 
app/__pycache__/main.cpython-314.pyc ADDED
Binary file (2.53 kB). View file
 
app/api/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """PhantomAPI — API package."""
app/api/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (212 Bytes). View file
 
app/api/__pycache__/custom.cpython-314.pyc ADDED
Binary file (2.17 kB). View file
 
app/api/custom.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PhantomAPI — Custom simplified API endpoint."""
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException
4
+ from pydantic import BaseModel
5
+ from app.services.browser import engine
6
+
7
+ router = APIRouter(tags=["custom"])
8
+
9
+ class QuickPromptRequest(BaseModel):
10
+ prompt: str
11
+
12
+ class QuickPromptResponse(BaseModel):
13
+ status: str
14
+ text: str
15
+
16
+ @router.post("/api", response_model=QuickPromptResponse)
17
+ async def quick_prompt(request: QuickPromptRequest):
18
+ """Simple API endpoint for quick prompts."""
19
+ try:
20
+ # Get response from browser engine
21
+ response_text = engine.chat(request.prompt)
22
+
23
+ return QuickPromptResponse(
24
+ status="success",
25
+ text=response_text
26
+ )
27
+ except Exception as e:
28
+ return QuickPromptResponse(
29
+ status="error",
30
+ text=str(e)
31
+ )
app/api/v1/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """PhantomAPI — API v1 package."""
app/api/v1/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (218 Bytes). View file
 
app/api/v1/__pycache__/chat.cpython-314.pyc ADDED
Binary file (1.19 kB). View file
 
app/api/v1/__pycache__/models.cpython-314.pyc ADDED
Binary file (701 Bytes). View file
 
app/api/v1/__pycache__/responses.cpython-314.pyc ADDED
Binary file (1.63 kB). View file
 
app/api/v1/__pycache__/router.cpython-314.pyc ADDED
Binary file (651 Bytes). View file
 
app/api/v1/chat.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PhantomAPI — POST /v1/chat/completions."""
2
+
3
+ from fastapi import APIRouter, Depends
4
+
5
+ from app.dependencies import verify_api_key
6
+ from app.schemas.chat import ChatCompletionRequest
7
+ from app.services.chat import process_chat_completion
8
+
9
+ router = APIRouter()
10
+
11
+
12
+ @router.post("/chat/completions", dependencies=[Depends(verify_api_key)])
13
+ async def chat_completions(payload: ChatCompletionRequest):
14
+ """OpenAI-compatible chat completions endpoint."""
15
+ data = payload.model_dump()
16
+ return process_chat_completion(data["messages"], data["model"], data.get("tools"))
app/api/v1/models.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PhantomAPI — GET /v1/models."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.get("/models")
9
+ async def list_models():
10
+ """Return available models for n8n model dropdown."""
11
+ return {
12
+ "object": "list",
13
+ "data": [
14
+ {"id": "gpt-4o-mini", "object": "model", "owned_by": "phantom-api"},
15
+ {"id": "gpt-4o", "object": "model", "owned_by": "phantom-api"},
16
+ ],
17
+ }
app/api/v1/responses.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PhantomAPI — POST /v1/responses."""
2
+
3
+ from fastapi import APIRouter, Depends
4
+
5
+ from app.dependencies import verify_api_key
6
+ from app.schemas.responses import ResponsesRequest
7
+ from app.services.chat import process_responses_api
8
+
9
+ router = APIRouter()
10
+
11
+
12
+ @router.post("/responses", dependencies=[Depends(verify_api_key)])
13
+ async def responses(payload: ResponsesRequest):
14
+ """Modern Responses API endpoint for newer n8n versions."""
15
+ data = payload.model_dump()
16
+ input_data = data.get("input", "")
17
+
18
+ # Normalise input to messages list
19
+ if isinstance(input_data, str):
20
+ messages = [{"role": "user", "content": input_data}]
21
+ elif isinstance(input_data, list):
22
+ messages = input_data
23
+ else:
24
+ messages = []
25
+
26
+ # Inject system instructions
27
+ instructions = data.get("instructions", "")
28
+ if instructions:
29
+ messages.insert(0, {"role": "system", "content": instructions})
30
+
31
+ return process_responses_api(messages, data["model"], data.get("tools"))
app/api/v1/router.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PhantomAPI — v1 API router aggregator."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from app.api.v1 import chat, responses, models
6
+
7
+ router = APIRouter(prefix="/v1", tags=["v1"])
8
+
9
+ router.include_router(chat.router)
10
+ router.include_router(responses.router)
11
+ router.include_router(models.router)
app/config.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PhantomAPI — Configuration via environment variables."""
2
+
3
+ from pydantic_settings import BaseSettings
4
+
5
+
6
+ class Settings(BaseSettings):
7
+ """Application settings loaded from .env file or environment variables."""
8
+
9
+ # --- Security ---
10
+ API_SECRET_KEY: str = "change-me-to-a-strong-secret"
11
+
12
+ # --- Server ---
13
+ HOST: str = "0.0.0.0"
14
+ PORT: int = 7860
15
+
16
+ # --- Browser Engine ---
17
+ HEADLESS: bool = True
18
+ BROWSER_TIMEOUT: int = 120000 # milliseconds
19
+
20
+ model_config = {
21
+ "env_file": ".env",
22
+ "env_file_encoding": "utf-8",
23
+ "extra": "ignore",
24
+ }
25
+
26
+
27
+ settings = Settings()
app/dependencies.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PhantomAPI — Authentication dependencies."""
2
+
3
+ from fastapi import Request, HTTPException
4
+ from app.config import settings
5
+
6
+
7
+ async def verify_api_key(request: Request) -> str:
8
+ """Validate the Bearer token from the Authorization header.
9
+
10
+ Returns the validated key on success, raises 401 on failure.
11
+ """
12
+ authorization = request.headers.get("authorization", "")
13
+
14
+ if not authorization:
15
+ raise HTTPException(status_code=401, detail="Missing Authorization header.")
16
+
17
+ # Strip "Bearer " prefix
18
+ token = authorization.replace("Bearer ", "").strip()
19
+
20
+ if token != settings.API_SECRET_KEY:
21
+ raise HTTPException(status_code=401, detail="Invalid API Key.")
22
+
23
+ return token
app/main.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PhantomAPI — Application factory."""
2
+
3
+ from contextlib import asynccontextmanager
4
+ from pathlib import Path
5
+
6
+ from fastapi import FastAPI
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import RedirectResponse
9
+ from fastapi.staticfiles import StaticFiles
10
+
11
+ from app import __version__
12
+ from app.api.v1.router import router as v1_router
13
+ from app.api.custom import router as custom_router
14
+ from app.services.browser import engine
15
+
16
+
17
+ @asynccontextmanager
18
+ async def lifespan(application: FastAPI):
19
+ """Start the browser engine on startup."""
20
+ engine.start()
21
+ yield
22
+
23
+
24
+ app = FastAPI(
25
+ title="PhantomAPI",
26
+ description="A proxy that turns free ChatGPT into an OpenAI-compatible API.",
27
+ version=__version__,
28
+ lifespan=lifespan,
29
+ )
30
+
31
+ app.add_middleware(
32
+ CORSMiddleware,
33
+ allow_origins=["*"],
34
+ allow_credentials=True,
35
+ allow_methods=["*"],
36
+ allow_headers=["*"],
37
+ )
38
+
39
+ app.include_router(v1_router)
40
+ app.include_router(custom_router)
41
+
42
+ static_dir = Path(__file__).parent.parent / "static"
43
+ if static_dir.exists():
44
+ app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
45
+
46
+
47
+ @app.get("/", tags=["health"])
48
+ async def health_check():
49
+ """Health check."""
50
+ return {"status": "running", "service": "PhantomAPI", "version": __version__}
51
+
52
+
53
+ @app.get("/gui", tags=["gui"])
54
+ async def gui_redirect():
55
+ """Redirect to Chat GUI."""
56
+ return RedirectResponse(url="/static/index.html")
app/schemas/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """PhantomAPI — Schemas package."""
app/schemas/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (220 Bytes). View file
 
app/schemas/__pycache__/chat.cpython-314.pyc ADDED
Binary file (2.98 kB). View file
 
app/schemas/__pycache__/responses.cpython-314.pyc ADDED
Binary file (1.73 kB). View file
 
app/schemas/chat.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PhantomAPI — Pydantic schemas for /v1/chat/completions."""
2
+
3
+ from typing import List, Dict, Any, Optional, Union
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class Message(BaseModel):
8
+ """A single message in the conversation."""
9
+ role: str = Field(..., description="The role of the message author.")
10
+ content: Union[str, List[Dict[str, Any]]] = Field(..., description="The content of the message.")
11
+ name: Optional[str] = Field(default=None, description="Name for tool messages.")
12
+ tool_calls: Optional[List[Dict[str, Any]]] = Field(default=None, description="Tool calls from assistant.")
13
+ tool_call_id: Optional[str] = Field(default=None, description="Tool call ID for tool responses.")
14
+
15
+
16
+ class ChatCompletionRequest(BaseModel):
17
+ """Request body for POST /v1/chat/completions."""
18
+ messages: List[Message] = Field(..., description="The conversation messages.")
19
+ model: str = Field(default="gpt-4o-mini", description="Model identifier.")
20
+ tools: Optional[List[Dict[str, Any]]] = Field(default=None, description="Available tools for function calling.")
21
+ temperature: Optional[float] = Field(default=None, description="Sampling temperature (ignored).")
22
+ max_tokens: Optional[int] = Field(default=None, description="Max tokens (ignored).")
23
+
24
+ model_config = {
25
+ "json_schema_extra": {
26
+ "examples": [{"messages": [{"role": "user", "content": "Hello!"}], "model": "gpt-4o-mini"}]
27
+ }
28
+ }
app/schemas/responses.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PhantomAPI — Pydantic schemas for /v1/responses."""
2
+
3
+ from typing import List, Dict, Any, Optional, Union
4
+ from pydantic import BaseModel, Field
5
+ from app.schemas.chat import Message
6
+
7
+
8
+ class ResponsesRequest(BaseModel):
9
+ """Request body for POST /v1/responses (Modern Responses API)."""
10
+ input: Union[str, List[Message]] = Field(..., description="The input text or messages.")
11
+ model: str = Field(default="gpt-4o-mini", description="Model identifier.")
12
+ tools: Optional[List[Dict[str, Any]]] = Field(default=None, description="A list of tools the model may call.")
13
+ instructions: Optional[str] = Field(default="", description="System instructions.")
14
+
15
+ model_config = {"json_schema_extra": {"examples": [{"input": "Hello!", "model": "gpt-4o-mini"}]}}
app/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """PhantomAPI — Services package."""
app/services/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (222 Bytes). View file
 
app/services/__pycache__/browser.cpython-314.pyc ADDED
Binary file (7.83 kB). View file
 
app/services/__pycache__/chat.cpython-314.pyc ADDED
Binary file (4.99 kB). View file
 
app/services/browser.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PhantomAPI — Browser automation engine.
2
+
3
+ Launches a persistent headless Chrome instance via Playwright
4
+ and interacts with chatgpt.com to generate responses.
5
+ """
6
+
7
+ import asyncio
8
+ import threading
9
+ from app.config import settings
10
+
11
+
12
+ class BrowserEngine(threading.Thread):
13
+ """A dedicated thread that runs an async Playwright browser.
14
+
15
+ This avoids blocking the FastAPI event loop while still giving
16
+ us a persistent browser instance that can handle sequential requests.
17
+ """
18
+
19
+ def __init__(self) -> None:
20
+ super().__init__(daemon=True)
21
+ self.loop = asyncio.new_event_loop()
22
+ self.ready = threading.Event()
23
+ self.browser = None
24
+ self.playwright = None
25
+
26
+ # ------------------------------------------------------------------
27
+ # Thread lifecycle
28
+ # ------------------------------------------------------------------
29
+
30
+ def run(self) -> None:
31
+ """Thread entry point — start browser and run the event loop forever."""
32
+ asyncio.set_event_loop(self.loop)
33
+ self.loop.run_until_complete(self._launch())
34
+ self.ready.set()
35
+ print("[PhantomAPI] ⚡ Browser engine ready.")
36
+ self.loop.run_forever()
37
+
38
+ async def _launch(self) -> None:
39
+ """Launch a stealth Chromium browser."""
40
+ from playwright.async_api import async_playwright
41
+
42
+ print("[PhantomAPI] 🚀 Launching browser...")
43
+ self.playwright = await async_playwright().start()
44
+ self.browser = await self.playwright.chromium.launch(
45
+ headless=settings.HEADLESS,
46
+ channel="chrome",
47
+ args=[
48
+ "--disable-blink-features=AutomationControlled",
49
+ "--no-sandbox",
50
+ "--disable-gpu",
51
+ "--disable-dev-shm-usage",
52
+ "--disable-setuid-sandbox",
53
+ ],
54
+ )
55
+
56
+ # ------------------------------------------------------------------
57
+ # Public API
58
+ # ------------------------------------------------------------------
59
+
60
+ def chat(self, prompt: str) -> str:
61
+ """Send a prompt to ChatGPT and return the response text.
62
+
63
+ This is a blocking call that schedules work on the browser
64
+ thread's event loop and waits for the result.
65
+ """
66
+ if not self.ready.wait(timeout=30) or self.browser is None:
67
+ raise RuntimeError("Browser engine is not ready. Is Chrome installed?")
68
+
69
+ future = asyncio.run_coroutine_threadsafe(
70
+ self._interact(prompt), self.loop
71
+ )
72
+ return future.result(timeout=settings.BROWSER_TIMEOUT // 1000 + 30)
73
+
74
+ # ------------------------------------------------------------------
75
+ # Private — browser interaction
76
+ # ------------------------------------------------------------------
77
+
78
+ async def _interact(self, prompt: str) -> str:
79
+ """Open a new ChatGPT session, send the prompt, and scrape the reply."""
80
+ context = await self.browser.new_context(
81
+ user_agent=(
82
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
83
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
84
+ "Chrome/124.0.0.0 Safari/537.36"
85
+ ),
86
+ viewport={"width": 1920, "height": 1080},
87
+ )
88
+
89
+ # Hide the webdriver flag so ChatGPT thinks we're a real user
90
+ await context.add_init_script(
91
+ "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
92
+ )
93
+
94
+ page = await context.new_page()
95
+
96
+ try:
97
+ page.set_default_timeout(settings.BROWSER_TIMEOUT)
98
+
99
+ # Navigate to ChatGPT
100
+ await page.goto("https://chatgpt.com/", wait_until="domcontentloaded")
101
+
102
+ # Type the prompt
103
+ await page.wait_for_selector("#prompt-textarea", timeout=60000)
104
+ await page.fill("#prompt-textarea", prompt)
105
+ await asyncio.sleep(0.5)
106
+ await page.press("#prompt-textarea", "Enter")
107
+
108
+ # Wait for the assistant to start responding
109
+ await page.wait_for_selector(
110
+ '[data-message-author-role="assistant"]',
111
+ timeout=settings.BROWSER_TIMEOUT,
112
+ )
113
+
114
+ # Poll until the response stabilises (no new text for ~2 seconds)
115
+ last_text = ""
116
+ unchanged_count = 0
117
+ while unchanged_count < 4:
118
+ elements = await page.query_selector_all(
119
+ '[data-message-author-role="assistant"]'
120
+ )
121
+ if elements:
122
+ current_text = await elements[-1].inner_text()
123
+ if current_text == last_text and current_text.strip():
124
+ unchanged_count += 1
125
+ else:
126
+ last_text = current_text
127
+ unchanged_count = 0
128
+ await asyncio.sleep(0.5)
129
+
130
+ return last_text.strip()
131
+
132
+ except Exception as exc:
133
+ print(f"[PhantomAPI] ❌ Browser error: {exc}")
134
+ raise
135
+ finally:
136
+ await page.close()
137
+ await context.close()
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Singleton — created once at import time, started in app lifespan
142
+ # ---------------------------------------------------------------------------
143
+ engine = BrowserEngine()
app/services/chat.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PhantomAPI — Chat completion service.
2
+
3
+ All business logic for processing chat requests lives here.
4
+ Route handlers call these functions and return the result directly.
5
+ """
6
+
7
+ import time
8
+ import uuid
9
+
10
+ from app.services.browser import engine
11
+ from app.utils.prompt import format_prompt
12
+ from app.utils.parser import parse_tool_calls
13
+
14
+
15
+ def process_chat_completion(messages: list, model: str, tools: list | None = None) -> dict:
16
+ """Process a chat completion request and return an OpenAI-compatible response."""
17
+ prompt = format_prompt(messages, tools=tools)
18
+ start = time.time()
19
+
20
+ print(f"[PhantomAPI] 📨 Request ({len(prompt)} chars)")
21
+ response_text = engine.chat(prompt)
22
+
23
+ p_tokens = len(prompt.split())
24
+ c_tokens = len(response_text.split())
25
+ tool_calls = parse_tool_calls(response_text) if tools else None
26
+
27
+ return _build_chat_response(response_text, tool_calls, model, int(start), p_tokens, c_tokens)
28
+
29
+
30
+ def process_responses_api(messages: list, model: str, tools: list | None = None) -> dict:
31
+ """Process a Responses API request and return the formatted response."""
32
+ prompt = format_prompt(messages, tools=tools)
33
+ start = time.time()
34
+
35
+ response_text = engine.chat(prompt)
36
+
37
+ p_tokens = len(prompt.split())
38
+ c_tokens = len(response_text.split())
39
+ tool_calls = parse_tool_calls(response_text) if tools else None
40
+
41
+ return _build_responses_response(response_text, tool_calls, model, int(start), p_tokens, c_tokens)
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Private response builders
46
+ # ---------------------------------------------------------------------------
47
+
48
+ def _build_chat_response(
49
+ text: str, tool_calls: list | None, model: str, created: int, p_tokens: int, c_tokens: int
50
+ ) -> dict:
51
+ """Build an OpenAI-compatible chat completion response dict."""
52
+ response_id = f"chatcmpl-{uuid.uuid4().hex[:29]}"
53
+ usage = {"prompt_tokens": p_tokens, "completion_tokens": c_tokens, "total_tokens": p_tokens + c_tokens}
54
+
55
+ if tool_calls:
56
+ message = {"role": "assistant", "content": None, "tool_calls": tool_calls}
57
+ finish_reason = "tool_calls"
58
+ else:
59
+ message = {"role": "assistant", "content": text}
60
+ finish_reason = "stop"
61
+
62
+ return {
63
+ "id": response_id,
64
+ "object": "chat.completion",
65
+ "created": created,
66
+ "model": model,
67
+ "choices": [{"index": 0, "message": message, "finish_reason": finish_reason}],
68
+ "usage": usage,
69
+ }
70
+
71
+
72
+ def _build_responses_response(
73
+ text: str, tool_calls: list | None, model: str, created: int, p_tokens: int, c_tokens: int
74
+ ) -> dict:
75
+ """Build a Responses API response dict."""
76
+ response_id = f"resp-{uuid.uuid4().hex[:29]}"
77
+ usage = {"input_tokens": p_tokens, "output_tokens": c_tokens, "total_tokens": p_tokens + c_tokens}
78
+
79
+ if tool_calls:
80
+ output = [
81
+ {
82
+ "type": "function_call",
83
+ "id": tc["id"],
84
+ "call_id": tc["id"],
85
+ "name": tc["function"]["name"],
86
+ "arguments": tc["function"]["arguments"],
87
+ "status": "completed",
88
+ }
89
+ for tc in tool_calls
90
+ ]
91
+ else:
92
+ output = [
93
+ {
94
+ "type": "message",
95
+ "role": "assistant",
96
+ "content": [{"type": "output_text", "text": text}],
97
+ }
98
+ ]
99
+
100
+ return {
101
+ "id": response_id,
102
+ "object": "response",
103
+ "created_at": created,
104
+ "model": model,
105
+ "status": "completed",
106
+ "output": output,
107
+ "usage": usage,
108
+ }
app/utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """PhantomAPI — Utils package."""
app/utils/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (216 Bytes). View file
 
app/utils/__pycache__/parser.cpython-314.pyc ADDED
Binary file (3.27 kB). View file