bibibi12345 commited on
Commit
5563f1a
·
1 Parent(s): 11df2b1
Files changed (4) hide show
  1. .gitignore +93 -0
  2. README.md +43 -6
  3. app.py +162 -0
  4. requirements.txt +5 -0
.gitignore ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a pyinstaller script; this is potentially
32
+ # problematic if you check your scripts into source control.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Environments
55
+ .env
56
+ .venv
57
+ env/
58
+ venv/
59
+ ENV/
60
+ env.bak/
61
+ venv.bak/
62
+
63
+ # Spyder project settings
64
+ .spyderproject
65
+ .spyderworkspace
66
+
67
+ # Rope project settings
68
+ .ropeproject
69
+
70
+ # mkdocs documentation
71
+ /site
72
+
73
+ # mypy
74
+ .mypy_cache/
75
+ .dmypy.json
76
+ dmypy.json
77
+
78
+ # Pyre type checker
79
+ .pyre/
80
+
81
+ # profiling data
82
+ *.prof
83
+ *.prof.*
84
+
85
+ # VS Code settings
86
+ .vscode/
87
+
88
+ # Mac specific
89
+ .DS_Store
90
+
91
+ # Dotenv environment variable files
92
+ .env.*
93
+ !.env.example
README.md CHANGED
@@ -1,11 +1,48 @@
1
  ---
2
- title: Helicone
3
- emoji: 🦀
4
- colorFrom: red
5
- colorTo: gray
6
- sdk: docker
 
7
  pinned: false
8
  license: mit
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: OpenAI Proxy Server
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: fastapi
7
+ app_file: app.py
8
  pinned: false
9
  license: mit
10
  ---
11
 
12
+ # OpenAI Format Proxy Server
13
+
14
+ This is a FastAPI proxy server designed to expose non-OpenAI standard endpoints (`https://us.helicone.ai/api/llm` for chat and `https://openrouter.ai/api/v1/models` for models) under the standard OpenAI API paths (`/v1/chat/completions` and `/v1/models`).
15
+
16
+ ## Features
17
+
18
+ * **OpenAI Compatibility:** Access the proxied endpoints using the standard OpenAI API structure.
19
+ * **Streaming Support:** Handles both streaming and non-streaming chat completion requests.
20
+ * **Authentication:** Protects the proxy server with Bearer token authentication (configure via `PROXY_API_KEY` environment variable).
21
+ * **Asynchronous:** Built with FastAPI for non-blocking, parallel request handling.
22
+ * **Hugging Face Ready:** Configured for easy deployment on Hugging Face Spaces.
23
+
24
+ ## Endpoints
25
+
26
+ * `GET /v1/models`: Proxies requests to `https://openrouter.ai/api/v1/models`. Requires `Authorization: Bearer <PROXY_API_KEY>`.
27
+ * `POST /v1/chat/completions`: Proxies requests to `https://us.helicone.ai/api/llm`. Requires `Authorization: Bearer <PROXY_API_KEY>`. Supports `stream: true`.
28
+ * `GET /health`: Health check endpoint.
29
+
30
+ ## Setup & Deployment (Hugging Face)
31
+
32
+ 1. Create a new Space on Hugging Face ([https://huggingface.co/new-space](https://huggingface.co/new-space)).
33
+ 2. Choose **FastAPI** as the SDK.
34
+ 3. Upload the files from this repository (`app.py`, `requirements.txt`, `README.md`).
35
+ 4. Go to the **Settings** tab of your Space.
36
+ 5. Under **Secrets**, add a new secret:
37
+ * **Name:** `PROXY_API_KEY`
38
+ * **Value:** Your desired secret API key for accessing *this proxy*.
39
+ 6. The Space should build and deploy automatically.
40
+
41
+ ## Local Development
42
+
43
+ 1. Clone the repository.
44
+ 2. Create a virtual environment: `python -m venv venv && source venv/bin/activate` (or `venv\Scripts\activate` on Windows).
45
+ 3. Install dependencies: `pip install -r requirements.txt`.
46
+ 4. Create a `.env` file in the root directory with the line: `PROXY_API_KEY=your_secret_key`
47
+ 5. Run the server: `uvicorn app:app --reload --port 8000`
48
+ 6. Access the proxy at `http://localhost:8000`. Use `Authorization: Bearer your_secret_key` in your requests.
app.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import httpx
3
+ import aiohttp
4
+ from fastapi import FastAPI, Request, HTTPException, Depends
5
+ from fastapi.responses import StreamingResponse, JSONResponse
6
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
7
+ from fastapi.middleware.cors import CORSMiddleware # Add CORS Middleware import
8
+ from dotenv import load_dotenv
9
+ import json
10
+
11
+ # Load environment variables from .env file
12
+ load_dotenv()
13
+
14
+ # Configuration
15
+ REMOTE_CHAT_COMPLETION_URL = "https://us.helicone.ai/api/llm"
16
+ REMOTE_MODELS_URL = "https://openrouter.ai/api/v1/models"
17
+ EXPECTED_API_KEY = os.getenv("PROXY_API_KEY", "default_insecure_key") # Load API key from .env or use a default
18
+
19
+ # --- Authentication ---
20
+ security = HTTPBearer()
21
+
22
+ async def verify_api_key(credentials: HTTPAuthorizationCredentials = Depends(security)):
23
+ """Verify the provided API key."""
24
+ if credentials.scheme != "Bearer" or credentials.credentials != EXPECTED_API_KEY:
25
+ raise HTTPException(status_code=401, detail="Invalid or missing API key")
26
+ return credentials.credentials
27
+
28
+ # --- FastAPI App ---
29
+ app = FastAPI(
30
+ title="OpenAI Format Proxy",
31
+ description="A proxy server that translates requests to an OpenAI-compatible format.",
32
+ version="1.0.0",
33
+ )
34
+
35
+ # --- CORS Middleware ---
36
+ # Allows requests from any origin, with any method and headers.
37
+ # Adjust origins if you need to restrict access to specific domains.
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=["*"], # Allows all origins
41
+ allow_credentials=True,
42
+ allow_methods=["*"], # Allows all methods (GET, POST, OPTIONS, etc.)
43
+ allow_headers=["*"], # Allows all headers
44
+ )
45
+
46
+ # --- Helper Functions ---
47
+ async def forward_request(request: Request, target_url: str):
48
+ """Forwards the request to the target URL, handling streaming."""
49
+ async with httpx.AsyncClient(timeout=None) as client: # Use httpx for simplicity in non-streaming and model requests
50
+ # Prepare headers, exclude Host header
51
+ headers = {key: value for key, value in request.headers.items() if key.lower() != 'host'}
52
+ headers["Authorization"] = f"Bearer {EXPECTED_API_KEY}" # Assuming remote API needs the same key? Or remove if not needed. Let's remove for now based on description.
53
+ headers.pop("authorization", None) # Remove incoming auth header before forwarding
54
+
55
+ # Read request body
56
+ body = await request.body()
57
+ req_data = {}
58
+ if body:
59
+ try:
60
+ req_data = json.loads(body.decode('utf-8'))
61
+ except json.JSONDecodeError:
62
+ raise HTTPException(status_code=400, detail="Invalid JSON body")
63
+
64
+
65
+ # Check for streaming
66
+ stream = req_data.get("stream", False)
67
+
68
+ # Prepare the request to the target server
69
+ rp_req = client.build_request(
70
+ method=request.method,
71
+ url=target_url,
72
+ headers=headers,
73
+ content=body, # Forward the original body
74
+ )
75
+
76
+ if stream:
77
+ # Use aiohttp for better streaming control if httpx causes issues, otherwise httpx stream is fine.
78
+ # Let's try httpx first
79
+ try:
80
+ async with client.stream(
81
+ request.method, target_url, headers=headers, content=body
82
+ ) as rp_resp:
83
+ # Check for non-200 status codes from the target server and raise HTTPException
84
+ if rp_resp.status_code != 200:
85
+ error_content = await rp_resp.aread()
86
+ raise HTTPException(status_code=rp_resp.status_code, detail=error_content.decode())
87
+
88
+ # Stream the response back
89
+ return StreamingResponse(
90
+ rp_resp.aiter_raw(),
91
+ status_code=rp_resp.status_code,
92
+ headers=dict(rp_resp.headers),
93
+ media_type=rp_resp.headers.get("content-type")
94
+ )
95
+ except httpx.RequestError as e:
96
+ raise HTTPException(status_code=502, detail=f"Error communicating with target server: {e}")
97
+
98
+ else:
99
+ # Handle non-streaming request
100
+ try:
101
+ rp_resp = await client.send(rp_req)
102
+ rp_resp.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
103
+ return JSONResponse(
104
+ content=rp_resp.json(), # Forward JSON response
105
+ status_code=rp_resp.status_code,
106
+ headers=dict(rp_resp.headers)
107
+ )
108
+ except httpx.HTTPStatusError as e:
109
+ # Forward the exact error response if possible
110
+ error_detail = e.response.text
111
+ try:
112
+ error_detail = e.response.json() # Try parsing as JSON
113
+ except json.JSONDecodeError:
114
+ pass # Keep as text if not JSON
115
+ raise HTTPException(status_code=e.response.status_code, detail=error_detail)
116
+ except httpx.RequestError as e:
117
+ raise HTTPException(status_code=502, detail=f"Error communicating with target server: {e}")
118
+
119
+
120
+ # --- API Endpoints ---
121
+
122
+ @app.get("/v1/models", dependencies=[Depends(verify_api_key)])
123
+ async def get_models(request: Request):
124
+ """Proxies requests to the remote models endpoint."""
125
+ async with httpx.AsyncClient(timeout=30.0) as client: # Shorter timeout for potentially faster models endpoint
126
+ try:
127
+ # We don't need to forward auth or body for models usually
128
+ headers = {key: value for key, value in request.headers.items() if key.lower() not in ['host', 'authorization']}
129
+ resp = await client.get(REMOTE_MODELS_URL, headers=headers)
130
+ resp.raise_for_status()
131
+ return JSONResponse(
132
+ content=resp.json(),
133
+ status_code=resp.status_code,
134
+ headers=dict(resp.headers)
135
+ )
136
+ except httpx.HTTPStatusError as e:
137
+ error_detail = e.response.text
138
+ try:
139
+ error_detail = e.response.json()
140
+ except json.JSONDecodeError:
141
+ pass
142
+ raise HTTPException(status_code=e.response.status_code, detail=error_detail)
143
+ except httpx.RequestError as e:
144
+ raise HTTPException(status_code=502, detail=f"Error communicating with models server: {e}")
145
+
146
+
147
+ @app.post("/v1/chat/completions", dependencies=[Depends(verify_api_key)])
148
+ async def chat_completions(request: Request):
149
+ """Proxies chat completion requests to the remote server, handling streaming."""
150
+ return await forward_request(request, REMOTE_CHAT_COMPLETION_URL)
151
+
152
+ # --- Health Check --- (Good practice for deployments)
153
+ @app.get("/health")
154
+ async def health_check():
155
+ """Simple health check endpoint."""
156
+ return {"status": "ok"}
157
+
158
+ # --- Main Execution --- (For local testing with uvicorn)
159
+ if __name__ == "__main__":
160
+ import uvicorn
161
+ port = int(os.getenv("PORT", 8000)) # Allow port configuration via env
162
+ uvicorn.run(app, host="0.0.0.0", port=port)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ httpx
4
+ python-dotenv
5
+ aiohttp # Added for efficient async streaming