Skip to content

Commit 50fcaf7

Browse files
committed
Refactoring
1 parent 5e88633 commit 50fcaf7

File tree

4 files changed

+77
-70
lines changed

4 files changed

+77
-70
lines changed

README.md

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# OpenRouter Proxy
22

3-
A simple proxy server for OpenRouter API that helps bypass rate limits on free API keys by rotating through multiple keys in a round-robin fashion.
3+
A simple proxy server for OpenRouter API that helps bypass rate limits on free API keys
4+
by rotating through multiple API keys in a round-robin fashion.
45

56
## Features
67

@@ -9,20 +10,21 @@ A simple proxy server for OpenRouter API that helps bypass rate limits on free A
910
- Automatically disables API keys temporarily when rate limits are reached
1011
- Streams responses chunk by chunk for efficient data transfer
1112
- Simple authentication for accessing the proxy
13+
- Uses OpenAI SDK for compatible endpoints for reliable handling
1214

1315
## Setup
1416

1517
1. Clone the repository
1618
2. Create a virtual environment and install dependencies:
17-
```
18-
python -m venv venv
19-
source venv/bin/activate # On Windows: venv\Scripts\activate
20-
pip install -r requirements.txt
21-
```
19+
```
20+
python -m venv venv
21+
source venv/bin/activate # On Windows: venv\Scripts\activate
22+
pip install -r requirements.txt
23+
```
2224
3. Create a configuration file:
23-
```
24-
cp config.yml.example config.yml
25-
```
25+
```
26+
cp config.yml.example config.yml
27+
```
2628
4. Edit `config.yml` to add your OpenRouter API keys and configure the server
2729
2830
## Configuration
@@ -36,13 +38,14 @@ server:
3638
port: 5555 # Port to listen on
3739
access_key: "your_local_access_key_here" # Authentication key
3840
log_level: "INFO" # Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
41+
http_log_level: "INFO" # HTTP access logs level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
3942
4043
# OpenRouter API keys
4144
openrouter:
4245
keys:
4346
- "sk-or-v1-your-first-api-key"
4447
- "sk-or-v1-your-second-api-key"
45-
rate_limit_cooldown: 7200 # Seconds to disable key after rate limit (2 hours)
48+
rate_limit_cooldown: 14400 # Seconds to disable key after rate limit (4 hours)
4649
```
4750

4851
## Usage
@@ -92,13 +95,21 @@ Authorization: Bearer your_local_access_key_here
9295

9396
## API Endpoints
9497

95-
The proxy supports all OpenRouter API v1 endpoints, including:
98+
The proxy supports all OpenRouter API v1 endpoints through the following endpoint:
99+
100+
- `/api/v1/{path}` - Proxies all requests to OpenRouter API v1
101+
102+
It also provides a health check endpoint:
103+
104+
- `/health` - Health check endpoint that returns `{"status": "ok"}`
105+
106+
## Dependencies
96107

97-
- `/api/v1/chat/completions` - Chat completions
98-
- `/api/v1/completions` - Text completions
99-
- `/api/v1/embeddings` - Text embeddings
100-
- `/api/v1/models` - List available models (no auth required)
101-
- `/api/v1/models/:author/:slug/endpoints` - Get specific model endpoints (no auth required)
108+
- FastAPI - Web framework
109+
- uvicorn - ASGI server
110+
- httpx - HTTP client
111+
- OpenAI - SDK for handling OpenAI-compatible endpoints
112+
- PyYAML - YAML parsing
102113

103114
## License
104115

routes.py

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,7 @@ async def proxy_endpoint(
5353

5454
# Verify authorization for non-public endpoints
5555
if not is_public:
56-
await verify_access_key(
57-
authorization=authorization,
58-
access_key=config["server"]["access_key"],
59-
)
56+
await verify_access_key(authorization=authorization)
6057

6158
# Log the full request URL including query parameters
6259
full_url = str(request.url).replace(str(request.base_url), "/")
@@ -68,7 +65,14 @@ async def proxy_endpoint(
6865
# Parse request body (if any)
6966
request_body = None
7067
is_stream = False
71-
68+
# Get API key to use
69+
if not is_public:
70+
api_key = await key_manager.get_next_key()
71+
if not api_key:
72+
raise HTTPException(status_code=503, detail="No available API keys")
73+
else:
74+
# For public endpoints, we don't need an API key
75+
api_key = ""
7276
try:
7377
body_bytes = await request.body()
7478
if body_bytes:
@@ -93,21 +97,12 @@ async def proxy_endpoint(
9397
logger.debug("Could not parse request body: %s", str(e))
9498
request_body = None
9599

96-
# For binary, models endpoint, non-OpenAI-compatible endpoints or requests with model-specific parameters, fall back to httpx
100+
# For models, non-OpenAI-compatible endpoints or requests with model-specific parameters, fall back to httpx
97101
if is_httpx or not is_openai:
98-
return await proxy_with_httpx(request, path, is_public, is_stream, is_completion)
102+
return await proxy_with_httpx(request, path, api_key, is_stream, is_completion)
99103

100104
# For OpenAI-compatible endpoints, use the OpenAI library
101105
try:
102-
# Get API key to use
103-
if not is_public:
104-
api_key = await key_manager.get_next_key()
105-
if not api_key:
106-
raise HTTPException(status_code=503, detail="No available API keys")
107-
else:
108-
# For public endpoints, we don't need an API key
109-
api_key = ""
110-
111106
# Create an OpenAI client
112107
client = await get_openai_client(api_key)
113108

@@ -119,7 +114,7 @@ async def proxy_endpoint(
119114
else:
120115
# Fallback for other endpoints
121116
return await proxy_with_httpx(
122-
request, path, is_public, is_stream, is_completion
117+
request, path, api_key, is_stream, is_completion
123118
)
124119

125120
except Exception as e:
@@ -234,10 +229,19 @@ async def stream_response() -> AsyncGenerator[bytes, None]:
234229
raise HTTPException(500, f"Error processing chat completion: {str(e)}") from e
235230

236231

232+
async def _check_httpx_err(body: str or bytes, api_key: str or None):
233+
if api_key and (isinstance(body, str) and body.startswith("data: ") or (
234+
isinstance(body, bytes) and body.startswith(b"data: "))):
235+
body = body[6:]
236+
has_rate_limit_error, reset_time_ms = check_rate_limit(body)
237+
if has_rate_limit_error:
238+
logger.warning("Rate limit detected in stream. Disabling key.")
239+
await key_manager.disable_key(api_key, reset_time_ms)
240+
237241
async def proxy_with_httpx(
238242
request: Request,
239243
path: str,
240-
is_public: bool,
244+
api_key: str,
241245
is_stream: bool,
242246
is_completion: bool,
243247
) -> Response:
@@ -260,20 +264,20 @@ async def proxy_with_httpx(
260264
if request.query_params:
261265
req_kwargs["url"] = f"{req_kwargs['url']}?{request.url.query}"
262266

263-
if not is_public:
264-
# For authenticated endpoints, use API key rotation
265-
api_key = await key_manager.get_next_key()
267+
if api_key:
266268
req_kwargs["headers"]["Authorization"] = f"Bearer {api_key}"
267269

268270

269271
openrouter_resp = await client.request(**req_kwargs)
270272
if not is_stream:
273+
body = await openrouter_resp.aread()
274+
await _check_httpx_err(body, api_key)
271275
return Response(
272-
content=await openrouter_resp.aread(),
276+
content=body,
273277
status_code=openrouter_resp.status_code,
274278
headers=dict(openrouter_resp.headers),
275279
)
276-
if is_public and not is_completion:
280+
if not api_key and not is_completion:
277281
return StreamingResponse(
278282
openrouter_resp.aiter_bytes(),
279283
status_code=openrouter_resp.status_code,
@@ -296,19 +300,13 @@ async def stream_completion():
296300
yield f"{line}\n\n".encode("utf-8")
297301
except Exception as err:
298302
logger.error("stream_completion error: %s", err)
299-
if not is_public and data.startswith('data: '):
300-
data = data[6:]
301-
has_rate_limit_error, reset_time_ms = check_rate_limit(data)
302-
if has_rate_limit_error:
303-
logger.warning("Rate limit detected in stream. Disabling key.")
304-
await key_manager.disable_key(api_key, reset_time_ms)
303+
await _check_httpx_err(data, api_key)
305304

306305
return StreamingResponse(
307306
stream_completion(),
308307
status_code=openrouter_resp.status_code,
309308
headers=dict(openrouter_resp.headers),
310309
)
311-
312310
except httpx.ConnectError as e:
313311
logger.error("Connection error to OpenRouter: %s", str(e))
314312
raise HTTPException(503, "Unable to connect to OpenRouter API") from e

test.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ async def test_openrouter_streaming():
5050
)
5151

5252
headers = {
53-
"HTTP-Referer": config.get("test", {}).get("http_referer", "http://localhost"), # Optional. Site URL for rankings on openrouter.ai.
54-
"X-Title": config.get("test", {}).get("x_title", "Local Test"), # Optional. Site title for rankings on openrouter.ai.
53+
# Optional. Site URL for rankings on openrouter.ai.
54+
"HTTP-Referer": config.get("test", {}).get("http_referer", "http://localhost"),
55+
# Optional. Site title for rankings on openrouter.ai.
56+
"X-Title": config.get("test", {}).get("x_title", "Local Test"),
5557
}
5658
if not ACCESS_KEY:
5759
print("No valid access key found. Request may fail if server requires authentication.")

utils.py

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from fastapi import Header, HTTPException
1111
from openai import APIError
1212

13-
from config import logger
13+
from config import config, logger
1414
from constants import RATE_LIMIT_ERROR_MESSAGE, RATE_LIMIT_ERROR_CODE
1515

1616

@@ -29,14 +29,12 @@ def get_local_ip() -> str:
2929

3030
async def verify_access_key(
3131
authorization: Optional[str] = Header(None),
32-
access_key: str = None,
3332
) -> bool:
3433
"""
3534
Verify the local access key for authentication.
3635
3736
Args:
3837
authorization: Authorization header
39-
access_key: Access key to verify
4038
4139
Returns:
4240
True if authentication is successful
@@ -52,7 +50,7 @@ async def verify_access_key(
5250
if scheme.lower() != "bearer":
5351
raise HTTPException(status_code=401, detail="Invalid authentication scheme")
5452

55-
if token != access_key:
53+
if token != config["server"]["access_key"]:
5654
raise HTTPException(status_code=401, detail="Invalid access key")
5755

5856
return True
@@ -74,7 +72,7 @@ def check_rate_limit_openai(err: APIError) -> Tuple[bool, Optional[int]]:
7472
try:
7573
reset_time_ms = int(err.body["metadata"]["headers"]["X-RateLimit-Reset"])
7674
has_rate_limit_error = True
77-
except Exception as _:
75+
except (TypeError, KeyError):
7876
pass
7977

8078
if reset_time_ms is None and RATE_LIMIT_ERROR_MESSAGE in err.message:
@@ -83,7 +81,7 @@ def check_rate_limit_openai(err: APIError) -> Tuple[bool, Optional[int]]:
8381
return has_rate_limit_error, reset_time_ms
8482

8583

86-
def check_rate_limit(data: str) -> Tuple[bool, Optional[int]]:
84+
def check_rate_limit(data: str or bytes) -> Tuple[bool, Optional[int]]:
8785
"""
8886
Check for rate limit error.
8987
@@ -99,21 +97,19 @@ def check_rate_limit(data: str) -> Tuple[bool, Optional[int]]:
9997
err = json.loads(data)
10098
except Exception as e:
10199
logger.warning('Json.loads error %s', e)
102-
return has_rate_limit_error, reset_time_ms
103-
if not isinstance(err, dict) or "error" not in err:
104-
return has_rate_limit_error, reset_time_ms
105-
106-
code = err["error"].get("code", 0)
107-
msg = err["error"].get("message", 0)
108-
try:
109-
x_rate_limit = int(err["error"]["metadata"]["headers"]["X-RateLimit-Reset"])
110-
except (TypeError, KeyError):
111-
x_rate_limit = None
112-
113-
if x_rate_limit :
114-
has_rate_limit_error = True
115-
reset_time_ms = x_rate_limit
116-
elif code == RATE_LIMIT_ERROR_CODE and msg == RATE_LIMIT_ERROR_MESSAGE:
117-
has_rate_limit_error = True
100+
else:
101+
if isinstance(err, dict) and "error" in err:
102+
code = err["error"].get("code", 0)
103+
msg = err["error"].get("message", 0)
104+
try:
105+
x_rate_limit = int(err["error"]["metadata"]["headers"]["X-RateLimit-Reset"])
106+
except (TypeError, KeyError):
107+
x_rate_limit = 0
108+
109+
if x_rate_limit > 0:
110+
has_rate_limit_error = True
111+
reset_time_ms = x_rate_limit
112+
elif code == RATE_LIMIT_ERROR_CODE and msg == RATE_LIMIT_ERROR_MESSAGE:
113+
has_rate_limit_error = True
118114

119115
return has_rate_limit_error, reset_time_ms

0 commit comments

Comments
 (0)