Skip to content

Commit 5130960

Browse files
committed
Improve error handling
1 parent fc43072 commit 5130960

File tree

5 files changed

+72
-52
lines changed

5 files changed

+72
-52
lines changed

config.yml.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ openrouter:
1616
- "sk-or-v1-your-third-api-key"
1717

1818
# Time in seconds to temporarily disable a key when rate limit is reached
19-
rate_limit_cooldown: 7200 # 2 hours
19+
rate_limit_cooldown: 14400 # 4 hours

key_manager.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from config import logger
1515

1616

17-
@staticmethod
1817
def _mask_key(key: str) -> str:
1918
"""Mask an API key for logging purposes."""
2019
if len(key) <= 8:

routes.py

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
import httpx
1010
from fastapi import APIRouter, Request, Header, HTTPException
1111
from fastapi.responses import StreamingResponse, Response
12-
from openai import AsyncOpenAI
12+
from openai import AsyncOpenAI, APIError
1313

1414
from config import config, logger
1515
from constants import OPENROUTER_BASE_URL, PUBLIC_ENDPOINTS, BINARY_ENDPOINTS
1616
from key_manager import KeyManager
1717
from utils import (
1818
verify_access_key,
1919
check_rate_limit_error,
20+
check_rate_limit_openai,
2021
)
2122

2223
# Create router
@@ -121,7 +122,7 @@ async def proxy_endpoint(
121122

122123
except Exception as e:
123124
logger.error("Error proxying request: %s", str(e))
124-
raise HTTPException(status_code=500, detail=f"Proxy error: {str(e)}")
125+
raise HTTPException(status_code=500, detail=f"Proxy error: {str(e)}") from e
125126

126127

127128
async def handle_chat_completions(
@@ -173,14 +174,17 @@ async def stream_response() -> AsyncGenerator[bytes, None]:
173174

174175
# Send the end marker
175176
yield b"data: [DONE]\n\n"
176-
except Exception as e:
177-
logger.error("Error in streaming response: %s", str(e))
177+
except APIError as err:
178+
logger.error("Error in streaming response: %s", err)
178179
# Check if this is a rate limit error
179-
if "rate limit" in str(e).lower() and api_key:
180-
logger.warning("Rate limit detected in stream. Disabling key.")
181-
await key_manager.disable_key(
182-
api_key, None
183-
) # Disable without reset time
180+
if api_key:
181+
has_rate_limit_error_, reset_time_ms_ = check_rate_limit_openai(err)
182+
if has_rate_limit_error_:
183+
logger.warning("Rate limit detected in stream. Disabling key.")
184+
await key_manager.disable_key(
185+
api_key, reset_time_ms_
186+
)
187+
184188

185189
# Return a streaming response
186190
return StreamingResponse(
@@ -199,29 +203,33 @@ async def stream_response() -> AsyncGenerator[bytes, None]:
199203
**completion_args, extra_headers=forward_headers, extra_body=extra_body
200204
)
201205

206+
result = response.model_dump()
207+
if 'error' in result:
208+
raise APIError(result['error'].get("message", "Error"), None, body=result['error'])
209+
202210
# Return the response as JSON
203211
return Response(
204-
content=json.dumps(response.model_dump()), media_type="application/json"
212+
content=json.dumps(result), media_type="application/json"
205213
)
206-
except Exception as e:
214+
except (APIError, Exception) as e:
207215
logger.error("Error in chat completions: %s", str(e))
208216
# Check if this is a rate limit error
209-
if "rate limit" in str(e).lower() and api_key:
210-
logger.warning(
211-
"Rate limit reached for API key. Disabling key and retrying."
212-
)
213-
await key_manager.disable_key(api_key, None)
214-
215-
# Try again with a new key
216-
new_api_key = await key_manager.get_next_key()
217-
if new_api_key:
218-
new_client = await get_openai_client(new_api_key)
219-
return await handle_chat_completions(
220-
new_client, request, request_body, new_api_key, is_stream
221-
)
217+
if api_key and isinstance(e, APIError):
218+
has_rate_limit_error, reset_time_ms = check_rate_limit_openai(e)
219+
if has_rate_limit_error:
220+
logger.warning("Rate limit detected in stream. Disabling key.")
221+
await key_manager.disable_key(api_key, reset_time_ms)
222+
223+
# Try again with a new key
224+
new_api_key = await key_manager.get_next_key()
225+
if new_api_key:
226+
new_client = await get_openai_client(new_api_key)
227+
return await handle_chat_completions(
228+
new_client, request, request_body, new_api_key, is_stream
229+
)
222230

223231
# Raise the exception
224-
raise HTTPException(500, f"Error processing chat completion: {str(e)}")
232+
raise HTTPException(500, f"Error processing chat completion: {str(e)}") from e
225233

226234

227235
async def proxy_with_httpx(
@@ -311,9 +319,7 @@ async def proxy_with_httpx(
311319
status_code=503,
312320
media_type="application/json",
313321
)
314-
raise HTTPException(
315-
status_code=503, detail="Unable to connect to OpenRouter API"
316-
)
322+
raise HTTPException(503, "Unable to connect to OpenRouter API") from e
317323

318324
# Handle binary responses
319325
if is_binary:
@@ -387,17 +393,13 @@ async def stream_sse():
387393

388394
except httpx.ConnectError as e:
389395
logger.error("Connection error to OpenRouter: %s", str(e))
390-
raise HTTPException(
391-
status_code=503, detail="Unable to connect to OpenRouter API"
392-
)
396+
raise HTTPException(503, "Unable to connect to OpenRouter API") from e
393397
except httpx.TimeoutException as e:
394398
logger.error("Timeout connecting to OpenRouter: %s", str(e))
395-
raise HTTPException(
396-
status_code=504, detail="OpenRouter API request timed out"
397-
)
399+
raise HTTPException(504, "OpenRouter API request timed out") from e
398400
except Exception as e:
399401
logger.error("Error proxying request with httpx: %s", str(e))
400-
raise HTTPException(status_code=500, detail=f"Proxy error: {str(e)}")
402+
raise HTTPException(500, f"Proxy error: {str(e)}") from e
401403

402404

403405
@router.get("/health")

test.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
from openai import AsyncOpenAI # Use the OpenAI library
1212

1313

14-
# Load configuration from config.yml
1514
def load_config():
16-
with open("config.yml", "r") as file:
15+
"""
16+
Load configuration from config.yml
17+
"""
18+
with open("config.yml", encoding="utf-8") as file:
1719
return yaml.safe_load(file)
1820

1921
# Get configuration
@@ -112,9 +114,9 @@ async def test_openrouter_streaming():
112114

113115
print("\n" + "-" * 50)
114116
if request_data["stream"]:
115-
print("\nStream completed!")
117+
print("\nStream completed!")
116118
else:
117-
print("\nNon-streaming response completed!")
119+
print("\nNon-streaming response completed!")
118120

119121
except Exception as e:
120122
print(f"Error occurred during test: {str(e)}")

utils.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from fastapi import Header, HTTPException
1010
from httpx import Response
11+
from openai import APIError
1112

1213
from config import logger
1314
from constants import RATE_LIMIT_ERROR_MESSAGE, RATE_LIMIT_ERROR_CODE
@@ -56,6 +57,30 @@ async def verify_access_key(
5657

5758
return True
5859

60+
def check_rate_limit_openai(err: APIError) -> Tuple[bool, Optional[int]]:
61+
"""
62+
Check for rate limit error.
63+
64+
Args:
65+
err: OpenAI APIError
66+
67+
Returns:
68+
Tuple (has_rate_limit_error, reset_time_ms)
69+
"""
70+
has_rate_limit_error = False
71+
reset_time_ms = None
72+
73+
if err.code == RATE_LIMIT_ERROR_CODE and isinstance(err.body, dict):
74+
try:
75+
reset_time_ms = int(err.body["metadata"]["headers"]["X-RateLimit-Reset"])
76+
has_rate_limit_error = True
77+
except Exception as _:
78+
pass
79+
80+
if reset_time_ms is None and RATE_LIMIT_ERROR_MESSAGE in err.message:
81+
has_rate_limit_error = True
82+
83+
return has_rate_limit_error, reset_time_ms
5984

6085
def check_rate_limit_error(response: Response) -> Tuple[bool, Optional[int]]:
6186
"""
@@ -70,14 +95,6 @@ def check_rate_limit_error(response: Response) -> Tuple[bool, Optional[int]]:
7095
has_rate_limit_error = False
7196
reset_time_ms = None
7297

73-
# Check headers
74-
if "X-RateLimit-Reset" in response.headers:
75-
try:
76-
reset_time_ms = int(response.headers["X-RateLimit-Reset"])
77-
logger.info(f"Found X-RateLimit-Reset in headers: {reset_time_ms}s", )
78-
except (ValueError, TypeError) as e:
79-
logger.warning(f"Failed to parse X-RateLimit-Reset header: {e}s", )
80-
8198
# Check response content if it's JSON
8299
content_type = response.headers.get('content-type', '')
83100
if 'application/json' in content_type:
@@ -93,10 +110,10 @@ def check_rate_limit_error(response: Response) -> Tuple[bool, Optional[int]]:
93110
reset_time_ms = int(data[
94111
"error"]["metadata"]["headers"]["X-RateLimit-Reset"])
95112
logger.info(
96-
f"Found X-RateLimit-Reset in response metadata: {reset_time_ms}")
113+
"Found X-RateLimit-Reset in response metadata: %s", reset_time_ms)
97114
except (ValueError, TypeError) as e:
98-
logger.warning(f"Failed to parse X-RateLimit-Reset from metadata: {e}s", )
115+
logger.warning("Failed to parse X-RateLimit-Reset from metadata: %s", e)
99116
except Exception as e:
100-
logger.debug(f"Error parsing JSON response: {e}s", )
117+
logger.debug("Error parsing JSON response: %s", e)
101118

102119
return has_rate_limit_error, reset_time_ms

0 commit comments

Comments
 (0)