Spaces:
Running
Running
update
Browse files- backend_api.py +56 -3
- frontend/src/app/page.tsx +33 -15
- frontend/src/lib/api.ts +6 -0
backend_api.py
CHANGED
|
@@ -90,7 +90,7 @@ def get_cached_client(model_id: str, provider: str = "auto"):
|
|
| 90 |
# Define models and languages here to avoid importing Gradio UI
|
| 91 |
AVAILABLE_MODELS = [
|
| 92 |
{"name": "Gemini 3.0 Pro", "id": "gemini-3.0-pro", "description": "Google Gemini 3.0 Pro via Poe with advanced reasoning"},
|
| 93 |
-
{"name": "Grok 4.1 Fast", "id": "x-ai/grok-4.1-fast", "description": "Grok 4.1 Fast model via OpenRouter"},
|
| 94 |
{"name": "MiniMax M2", "id": "MiniMaxAI/MiniMax-M2", "description": "MiniMax M2 model via HuggingFace InferenceClient with Novita provider"},
|
| 95 |
{"name": "DeepSeek V3.2-Exp", "id": "deepseek-ai/DeepSeek-V3.2-Exp", "description": "DeepSeek V3.2 Experimental via HuggingFace"},
|
| 96 |
{"name": "DeepSeek R1", "id": "deepseek-ai/DeepSeek-R1-0528", "description": "DeepSeek R1 model for code generation"},
|
|
@@ -490,16 +490,69 @@ async def generate_code(
|
|
| 490 |
yield f"data: {completion_data}\n\n"
|
| 491 |
|
| 492 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
error_data = json.dumps({
|
| 494 |
"type": "error",
|
| 495 |
-
"message":
|
| 496 |
})
|
| 497 |
yield f"data: {error_data}\n\n"
|
| 498 |
|
| 499 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
error_data = json.dumps({
|
| 501 |
"type": "error",
|
| 502 |
-
"message": f"Generation error: {
|
| 503 |
})
|
| 504 |
yield f"data: {error_data}\n\n"
|
| 505 |
|
|
|
|
| 90 |
# Define models and languages here to avoid importing Gradio UI
|
| 91 |
AVAILABLE_MODELS = [
|
| 92 |
{"name": "Gemini 3.0 Pro", "id": "gemini-3.0-pro", "description": "Google Gemini 3.0 Pro via Poe with advanced reasoning"},
|
| 93 |
+
{"name": "Grok 4.1 Fast", "id": "x-ai/grok-4.1-fast", "description": "Grok 4.1 Fast model via OpenRouter (20 req/min on free tier)"},
|
| 94 |
{"name": "MiniMax M2", "id": "MiniMaxAI/MiniMax-M2", "description": "MiniMax M2 model via HuggingFace InferenceClient with Novita provider"},
|
| 95 |
{"name": "DeepSeek V3.2-Exp", "id": "deepseek-ai/DeepSeek-V3.2-Exp", "description": "DeepSeek V3.2 Experimental via HuggingFace"},
|
| 96 |
{"name": "DeepSeek R1", "id": "deepseek-ai/DeepSeek-R1-0528", "description": "DeepSeek R1 model for code generation"},
|
|
|
|
| 490 |
yield f"data: {completion_data}\n\n"
|
| 491 |
|
| 492 |
except Exception as e:
|
| 493 |
+
# Handle rate limiting and other API errors
|
| 494 |
+
error_message = str(e)
|
| 495 |
+
is_rate_limit = False
|
| 496 |
+
error_type = type(e).__name__
|
| 497 |
+
|
| 498 |
+
# Check for OpenAI SDK rate limit errors
|
| 499 |
+
if error_type == "RateLimitError" or "rate_limit" in error_type.lower():
|
| 500 |
+
is_rate_limit = True
|
| 501 |
+
# Check if this is a rate limit error (429 status code)
|
| 502 |
+
elif hasattr(e, 'status_code') and e.status_code == 429:
|
| 503 |
+
is_rate_limit = True
|
| 504 |
+
# Check error message for rate limit indicators
|
| 505 |
+
elif "429" in error_message or "rate limit" in error_message.lower() or "too many requests" in error_message.lower():
|
| 506 |
+
is_rate_limit = True
|
| 507 |
+
|
| 508 |
+
if is_rate_limit:
|
| 509 |
+
# Try to extract retry-after header or message
|
| 510 |
+
retry_after = None
|
| 511 |
+
if hasattr(e, 'response') and e.response:
|
| 512 |
+
retry_after = e.response.headers.get('Retry-After') or e.response.headers.get('retry-after')
|
| 513 |
+
# Also check if the error object has retry_after
|
| 514 |
+
elif hasattr(e, 'retry_after'):
|
| 515 |
+
retry_after = str(e.retry_after)
|
| 516 |
+
|
| 517 |
+
if selected_model_id == "x-ai/grok-4.1-fast" or selected_model_id.startswith("openrouter/"):
|
| 518 |
+
error_message = "⏱️ Rate limit exceeded for OpenRouter model"
|
| 519 |
+
if retry_after:
|
| 520 |
+
error_message += f". Please wait {retry_after} seconds before trying again."
|
| 521 |
+
else:
|
| 522 |
+
error_message += ". Free tier allows up to 20 requests per minute. Please wait a moment and try again."
|
| 523 |
+
else:
|
| 524 |
+
error_message = f"⏱️ Rate limit exceeded. Please wait before trying again."
|
| 525 |
+
if retry_after:
|
| 526 |
+
error_message += f" Retry after {retry_after} seconds."
|
| 527 |
+
|
| 528 |
+
# Check for other common API errors
|
| 529 |
+
elif hasattr(e, 'status_code'):
|
| 530 |
+
if e.status_code == 401:
|
| 531 |
+
error_message = "❌ Authentication failed. Please check your API key."
|
| 532 |
+
elif e.status_code == 403:
|
| 533 |
+
error_message = "❌ Access forbidden. Please check your API key permissions."
|
| 534 |
+
elif e.status_code == 500 or e.status_code == 502 or e.status_code == 503:
|
| 535 |
+
error_message = "❌ Service temporarily unavailable. Please try again later."
|
| 536 |
+
|
| 537 |
error_data = json.dumps({
|
| 538 |
"type": "error",
|
| 539 |
+
"message": error_message
|
| 540 |
})
|
| 541 |
yield f"data: {error_data}\n\n"
|
| 542 |
|
| 543 |
except Exception as e:
|
| 544 |
+
# Fallback error handling
|
| 545 |
+
error_message = str(e)
|
| 546 |
+
# Check if it's a rate limit error in the exception message
|
| 547 |
+
if "429" in error_message or "rate limit" in error_message.lower() or "too many requests" in error_message.lower():
|
| 548 |
+
if selected_model_id == "x-ai/grok-4.1-fast" or selected_model_id.startswith("openrouter/"):
|
| 549 |
+
error_message = "⏱️ Rate limit exceeded for OpenRouter model. Free tier allows up to 20 requests per minute. Please wait a moment and try again."
|
| 550 |
+
else:
|
| 551 |
+
error_message = "⏱️ Rate limit exceeded. Please wait before trying again."
|
| 552 |
+
|
| 553 |
error_data = json.dumps({
|
| 554 |
"type": "error",
|
| 555 |
+
"message": f"Generation error: {error_message}"
|
| 556 |
})
|
| 557 |
yield f"data: {error_data}\n\n"
|
| 558 |
|
frontend/src/app/page.tsx
CHANGED
|
@@ -59,6 +59,8 @@ export default function Home() {
|
|
| 59 |
|
| 60 |
// Track if we've attempted to fetch username to avoid repeated failures
|
| 61 |
const usernameFetchAttemptedRef = useRef(false);
|
|
|
|
|
|
|
| 62 |
|
| 63 |
// Check auth on mount and handle OAuth callback
|
| 64 |
useEffect(() => {
|
|
@@ -68,8 +70,9 @@ export default function Home() {
|
|
| 68 |
// initializeOAuth already handles this, but we call checkAuth to sync state
|
| 69 |
const urlParams = new URLSearchParams(window.location.search);
|
| 70 |
if (urlParams.get('session')) {
|
| 71 |
-
// OAuth callback - reset
|
| 72 |
usernameFetchAttemptedRef.current = false;
|
|
|
|
| 73 |
setTimeout(() => checkAuth(), 200);
|
| 74 |
}
|
| 75 |
}, []); // Only run once on mount
|
|
@@ -79,8 +82,11 @@ export default function Home() {
|
|
| 79 |
useEffect(() => {
|
| 80 |
const handleStorageChange = (e: StorageEvent) => {
|
| 81 |
if (e.key === 'hf_oauth_token' || e.key === 'hf_user_info') {
|
| 82 |
-
//
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
| 84 |
checkAuth();
|
| 85 |
}
|
| 86 |
};
|
|
@@ -90,10 +96,16 @@ export default function Home() {
|
|
| 90 |
}, []);
|
| 91 |
|
| 92 |
// Listen for window focus (user returns to tab after OAuth redirect)
|
|
|
|
| 93 |
useEffect(() => {
|
| 94 |
const handleFocus = () => {
|
| 95 |
-
//
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
checkAuth();
|
| 98 |
};
|
| 99 |
|
|
@@ -111,18 +123,18 @@ export default function Home() {
|
|
| 111 |
if (token) {
|
| 112 |
apiClient.setToken(token);
|
| 113 |
|
| 114 |
-
// Get username from auth status (only if we don't have it yet and
|
| 115 |
-
//
|
| 116 |
-
if (!username && !usernameFetchAttemptedRef.current) {
|
| 117 |
usernameFetchAttemptedRef.current = true;
|
| 118 |
try {
|
| 119 |
const authStatus = await apiClient.getAuthStatus();
|
| 120 |
if (authStatus.username) {
|
| 121 |
setUsername(authStatus.username);
|
|
|
|
| 122 |
}
|
| 123 |
} catch (error: any) {
|
| 124 |
-
//
|
| 125 |
-
// Connection errors mean backend isn't available, which is OK for client-side auth
|
| 126 |
const isConnectionError =
|
| 127 |
error.code === 'ECONNABORTED' ||
|
| 128 |
error.code === 'ECONNRESET' ||
|
|
@@ -133,12 +145,16 @@ export default function Home() {
|
|
| 133 |
error.response?.status === 503 ||
|
| 134 |
error.response?.status === 502;
|
| 135 |
|
| 136 |
-
if (
|
| 137 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
console.error('Failed to get username:', error);
|
|
|
|
| 139 |
}
|
| 140 |
-
// Reset attempt flag so we can try again later (e.g., when backend comes up)
|
| 141 |
-
usernameFetchAttemptedRef.current = false;
|
| 142 |
}
|
| 143 |
}
|
| 144 |
} else {
|
|
@@ -148,14 +164,16 @@ export default function Home() {
|
|
| 148 |
setUsername(null);
|
| 149 |
}
|
| 150 |
usernameFetchAttemptedRef.current = false;
|
|
|
|
| 151 |
}
|
| 152 |
} else {
|
| 153 |
-
// Not authenticated - clear username and reset
|
| 154 |
apiClient.setToken(null);
|
| 155 |
if (username) {
|
| 156 |
setUsername(null);
|
| 157 |
}
|
| 158 |
usernameFetchAttemptedRef.current = false;
|
|
|
|
| 159 |
}
|
| 160 |
};
|
| 161 |
|
|
|
|
| 59 |
|
| 60 |
// Track if we've attempted to fetch username to avoid repeated failures
|
| 61 |
const usernameFetchAttemptedRef = useRef(false);
|
| 62 |
+
// Track if backend appears to be unavailable (to avoid repeated failed requests)
|
| 63 |
+
const backendUnavailableRef = useRef(false);
|
| 64 |
|
| 65 |
// Check auth on mount and handle OAuth callback
|
| 66 |
useEffect(() => {
|
|
|
|
| 70 |
// initializeOAuth already handles this, but we call checkAuth to sync state
|
| 71 |
const urlParams = new URLSearchParams(window.location.search);
|
| 72 |
if (urlParams.get('session')) {
|
| 73 |
+
// OAuth callback - reset both flags and check auth after a brief delay
|
| 74 |
usernameFetchAttemptedRef.current = false;
|
| 75 |
+
backendUnavailableRef.current = false; // Reset backend status on OAuth callback
|
| 76 |
setTimeout(() => checkAuth(), 200);
|
| 77 |
}
|
| 78 |
}, []); // Only run once on mount
|
|
|
|
| 82 |
useEffect(() => {
|
| 83 |
const handleStorageChange = (e: StorageEvent) => {
|
| 84 |
if (e.key === 'hf_oauth_token' || e.key === 'hf_user_info') {
|
| 85 |
+
// Only reset username fetch if we have a token (might be logging in)
|
| 86 |
+
if (e.newValue) {
|
| 87 |
+
usernameFetchAttemptedRef.current = false;
|
| 88 |
+
backendUnavailableRef.current = false; // Reset backend status on login
|
| 89 |
+
}
|
| 90 |
checkAuth();
|
| 91 |
}
|
| 92 |
};
|
|
|
|
| 96 |
}, []);
|
| 97 |
|
| 98 |
// Listen for window focus (user returns to tab after OAuth redirect)
|
| 99 |
+
// Only check if backend was available before or if we're authenticated with token
|
| 100 |
useEffect(() => {
|
| 101 |
const handleFocus = () => {
|
| 102 |
+
// Only reset and check if we're authenticated (might have logged in elsewhere)
|
| 103 |
+
// Don't reset if backend is known to be unavailable and we're not authenticated
|
| 104 |
+
const authenticated = checkIsAuthenticated();
|
| 105 |
+
if (authenticated) {
|
| 106 |
+
usernameFetchAttemptedRef.current = false;
|
| 107 |
+
backendUnavailableRef.current = false; // Reset backend status - might be back up
|
| 108 |
+
}
|
| 109 |
checkAuth();
|
| 110 |
};
|
| 111 |
|
|
|
|
| 123 |
if (token) {
|
| 124 |
apiClient.setToken(token);
|
| 125 |
|
| 126 |
+
// Get username from auth status (only if we don't have it yet and backend is available)
|
| 127 |
+
// Skip if backend is known to be unavailable to avoid repeated failed requests
|
| 128 |
+
if (!username && !usernameFetchAttemptedRef.current && !backendUnavailableRef.current) {
|
| 129 |
usernameFetchAttemptedRef.current = true;
|
| 130 |
try {
|
| 131 |
const authStatus = await apiClient.getAuthStatus();
|
| 132 |
if (authStatus.username) {
|
| 133 |
setUsername(authStatus.username);
|
| 134 |
+
backendUnavailableRef.current = false; // Backend is working
|
| 135 |
}
|
| 136 |
} catch (error: any) {
|
| 137 |
+
// Check if this is a connection error
|
|
|
|
| 138 |
const isConnectionError =
|
| 139 |
error.code === 'ECONNABORTED' ||
|
| 140 |
error.code === 'ECONNRESET' ||
|
|
|
|
| 145 |
error.response?.status === 503 ||
|
| 146 |
error.response?.status === 502;
|
| 147 |
|
| 148 |
+
if (isConnectionError) {
|
| 149 |
+
// Mark backend as unavailable to avoid repeated requests
|
| 150 |
+
backendUnavailableRef.current = true;
|
| 151 |
+
// Don't reset attempt flag - keep it true so we don't retry until explicitly reset
|
| 152 |
+
// This prevents repeated failed requests when backend is down
|
| 153 |
+
} else {
|
| 154 |
+
// Non-connection error - log it and reset attempt flag
|
| 155 |
console.error('Failed to get username:', error);
|
| 156 |
+
usernameFetchAttemptedRef.current = false;
|
| 157 |
}
|
|
|
|
|
|
|
| 158 |
}
|
| 159 |
}
|
| 160 |
} else {
|
|
|
|
| 164 |
setUsername(null);
|
| 165 |
}
|
| 166 |
usernameFetchAttemptedRef.current = false;
|
| 167 |
+
backendUnavailableRef.current = false;
|
| 168 |
}
|
| 169 |
} else {
|
| 170 |
+
// Not authenticated - clear username and reset flags
|
| 171 |
apiClient.setToken(null);
|
| 172 |
if (username) {
|
| 173 |
setUsername(null);
|
| 174 |
}
|
| 175 |
usernameFetchAttemptedRef.current = false;
|
| 176 |
+
// Keep backendUnavailableRef as is - it's useful information even when not authenticated
|
| 177 |
}
|
| 178 |
};
|
| 179 |
|
frontend/src/lib/api.ts
CHANGED
|
@@ -177,6 +177,12 @@ class ApiClient {
|
|
| 177 |
signal: abortController.signal,
|
| 178 |
})
|
| 179 |
.then(async (response) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
if (!response.ok) {
|
| 181 |
throw new Error(`HTTP error! status: ${response.status}`);
|
| 182 |
}
|
|
|
|
| 177 |
signal: abortController.signal,
|
| 178 |
})
|
| 179 |
.then(async (response) => {
|
| 180 |
+
// Handle rate limit errors before parsing response
|
| 181 |
+
if (response.status === 429) {
|
| 182 |
+
onError('⏱️ Rate limit exceeded. Free tier allows up to 20 requests per minute. Please wait a moment and try again.');
|
| 183 |
+
return;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
if (!response.ok) {
|
| 187 |
throw new Error(`HTTP error! status: ${response.status}`);
|
| 188 |
}
|