File size: 10,272 Bytes
596966d
59afa9c
e870341
8fb26a9
49deb9f
47fec89
e870341
47fec89
 
 
ca6e5e3
 
 
 
 
 
 
 
 
 
 
 
 
575f4f2
ca6e5e3
 
 
 
e349196
ca6e5e3
 
575f4f2
 
 
 
ca6e5e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575f4f2
ca6e5e3
 
575f4f2
8fb26a9
575f4f2
ca6e5e3
8fb26a9
575f4f2
 
 
 
 
 
 
 
8fb26a9
575f4f2
8fb26a9
 
575f4f2
 
 
 
 
 
 
 
 
 
 
 
8fb26a9
 
 
 
 
 
 
 
 
 
575f4f2
8fb26a9
 
 
 
 
 
 
 
 
ca6e5e3
575f4f2
cc5adba
575f4f2
cc5adba
 
 
 
 
 
 
 
 
 
 
575f4f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cc5adba
 
575f4f2
cc5adba
 
ca6e5e3
 
 
 
 
 
 
 
 
 
 
cc5adba
 
ca6e5e3
 
 
 
 
 
 
 
 
 
 
 
bd85893
cc5adba
 
 
 
 
ca6e5e3
 
 
 
 
 
 
8fb26a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import os
import time
import logging
import asyncio
import httpx
from fastapi import FastAPI, HTTPException, Header, Depends
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from typing import List, Optional

# --- CUSTOM LOG FILTER TO BLOCK BOT TRAFFIC FROM THE CONSOLE ---
class BotLogFilter(logging.Filter):
    def filter(self, record: logging.LogRecord) -> bool:
        msg = record.getMessage()
        if "GET /v1/" in msg or "POST /v1/" in msg or "Started server" in msg or "Application" in msg or "Uvicorn running" in msg:
            return True
        if "HTTP" in msg:
            return False
        return True

logging.getLogger("uvicorn.access").addFilter(BotLogFilter())
# ---------------------------------------------------------------

app = FastAPI(title="GenAI Multi-Proxy Server (Rate-Limit Protected)")

AGNES_API_KEY = os.getenv("AGNES_API_KEY")
REQUIRED_STATIC_TOKEN = "sk-apikeyyoudummy"

# Global timeout config
TIMEOUT_CONFIG = httpx.Timeout(320.0, connect=30.0, read=300.0, write=30.0)

# Video Rate Limiter Lock & Timestamp
video_lock = asyncio.Lock()
last_video_time = 0.0

class ChatMessage(BaseModel):
    role: str
    content: str

class ChatCompletionRequest(BaseModel):
    model: str = "agnes-image-2.1-flash-1k"
    messages: List[ChatMessage]
    temperature: Optional[float] = 1.0
    stream: Optional[bool] = False

async def verify_static_token(authorization: Optional[str] = Header(None)) -> str:
    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Missing or malformed Authorization header.")
    token = authorization.split(" ")[1]
    if token != REQUIRED_STATIC_TOKEN:
        raise HTTPException(status_code=403, detail="Invalid API Key provided.")
    if not AGNES_API_KEY:
        raise HTTPException(status_code=500, detail="Server misconfiguration: AGNES_API_KEY env variable not set.")
    return AGNES_API_KEY

# --- IMAGE HANDLER WITH 2K -> 1K FALLBACK ---
async def handle_image_generation(prompt: str, target_size: str, upstream_key: str) -> str:
    headers = {"Authorization": f"Bearer {upstream_key}", "Content-Type": "application/json"}
    current_size = target_size
    max_retries = 3
    
    async with httpx.AsyncClient(timeout=TIMEOUT_CONFIG) as client:
        for attempt in range(max_retries):
            payload = {
                "model": "agnes-image-2.1-flash",
                "prompt": prompt,
                "size": current_size,
                "ratio": "1:1",
                "extra_body": {"response_format": "url"}
            }
            
            try:
                print(f"[PROXY LOG] -> Sending image request ({current_size}) [Attempt {attempt + 1}/{max_retries}]...")
                res = await client.post("https://apihub.agnes-ai.com/v1/images/generations", json=payload, headers=headers)
                
                # Check for Rate Limit 429
                if res.status_code == 429:
                    if current_size == "2K":
                        print(f"[PROXY LOG] !!! 2K tier rate limit hit! Automatically falling back to 1K tier...")
                        current_size = "1K"
                        await asyncio.sleep(0.5)
                        continue  # Retry immediately using 1K payload
                    else:
                        print(f"[PROXY LOG] !!! Agnes Rate Limit Hit on 1K tier: {res.text}")
                        raise HTTPException(status_code=429, detail="Upstream rate limit reached. Please wait a moment before trying again.")

                # Upstream busy or temporary error retries
                if res.status_code in [502, 503, 504] or "upstream_error" in res.text or "do_request_failed" in res.text:
                    print(f"[PROXY LOG] !!! Upstream busy or network hiccup. Retrying in 1.5 seconds...")
                    await asyncio.sleep(1.5)
                    continue
                    
                if res.status_code != 200:
                    print(f"[PROXY LOG] !!! Agnes API Error: {res.text}")
                    raise HTTPException(status_code=res.status_code, detail=f"Agnes Image Error: {res.text}")
                
                img_url = res.json()["data"][0]["url"]
                print(f"[PROXY LOG] -> Agnes image ready ({current_size}). Returning direct URL: {img_url}")
                return img_url
                
            except httpx.RequestError as exc:
                print(f"[PROXY LOG] !!! Connection exception on attempt {attempt + 1}: {exc}")
                if attempt == max_retries - 1:
                    raise HTTPException(status_code=503, detail="Agnes AI gateway completely unreachable.")
                await asyncio.sleep(1.5)
                
        raise HTTPException(status_code=503, detail="Agnes AI backend is currently overloaded. Please try again.")

# --- RATE-LIMITED VIDEO HANDLER ---
async def handle_video_generation(prompt: str, upstream_key: str) -> str:
    global last_video_time
    headers = {"Authorization": f"Bearer {upstream_key}", "Content-Type": "application/json"}
    
    payload = {
        "model": "agnes-video-v2.0",
        "prompt": prompt,
        "width": 832,
        "height": 448,
        "num_frames": 81,
        "frame_rate": 24
    }
    
    # Enforce 1 request per minute queue locally
    async with video_lock:
        elapsed = time.time() - last_video_time
        if elapsed < 60.0:
            wait_time = 60.0 - elapsed
            print(f"[PROXY LOG] -> Video rate limit throttle: queueing request for {wait_time:.1f}s...")
            await asyncio.sleep(wait_time)
            
        max_retries = 3
        async with httpx.AsyncClient(timeout=TIMEOUT_CONFIG) as client:
            for attempt in range(max_retries):
                try:
                    print(f"[PROXY LOG] -> Submitting video task to Agnes [Attempt {attempt + 1}/{max_retries}]...")
                    res = await client.post("https://apihub.agnes-ai.com/v1/videos", json=payload, headers=headers)
                    
                    if res.status_code == 429:
                        print(f"[PROXY LOG] !!! Agnes Video Rate Limit Hit: {res.text}")
                        raise HTTPException(status_code=429, detail="Video engine rate limit reached (1 request/min). Please try again shortly.")

                    if res.status_code in [502, 503, 504] or "upstream_error" in res.text or "do_request_failed" in res.text:
                        print(f"[PROXY LOG] !!! Agnes busy or error. Retrying task submission in 1.5s...")
                        await asyncio.sleep(1.5)
                        continue
                        
                    if res.status_code != 200:
                        print(f"[PROXY LOG] !!! Agnes Task Creation Error: {res.text}")
                        raise HTTPException(status_code=res.status_code, detail=f"Agnes API Error: {res.text}")
                    
                    task_data = res.json()
                    video_id = task_data.get("video_id")
                    
                    # Update successful generation timestamp
                    last_video_time = time.time()
                    
                    tracking_url = f"https://apihub.agnes-ai.com/agnesapi?video_id={video_id}"
                    print(f"[PROXY LOG] -> Task successfully queued. Returning tracking URL immediately: {tracking_url}")
                    return tracking_url
                    
                except httpx.RequestError as exc:
                    print(f"[PROXY LOG] !!! Network exception during task creation: {exc}")
                    if attempt == max_retries - 1:
                        raise HTTPException(status_code=503, detail="Agnes video creation gateway completely unreachable.")
                    await asyncio.sleep(1.5)
                    
            raise HTTPException(status_code=503, detail="Agnes backend is currently overloaded. Please try again.")

# --- ROUTES ---
@app.get("/", response_class=HTMLResponse)
async def read_root():
    return "<html><body></body></html>"

@app.get("/v1/models")
async def list_models():
    now = int(time.time())
    return {
        "object": "list",
        "data": [
            {"id": "agnes-image-2.1-flash-1k", "object": "model", "created": now, "owned_by": "custom-proxy"},
            {"id": "agnes-image-2.1-flash-2k", "object": "model", "created": now, "owned_by": "custom-proxy"},
            {"id": "agnes-video-v2.0", "object": "model", "created": now, "owned_by": "custom-proxy"}
        ]
    }

@app.post("/v1/chat/completions")
async def chat_completions_proxy(request: ChatCompletionRequest, upstream_key: str = Depends(verify_static_token)):
    print(f"[PROXY LOG] === Incoming Request Received ===")
    if not request.messages:
        raise HTTPException(status_code=400, detail="No messages provided.")
    
    user_prompt = request.messages[-1].content
    model_name = request.model.lower() if request.model else "agnes-image-2.1-flash-1k"
    
    # Routing matching target model selection
    if "video" in model_name:
        final_url = await handle_video_generation(user_prompt, upstream_key)
    else:
        target_size = "2K" if "-2k" in model_name else "1K"
        final_url = await handle_image_generation(user_prompt, target_size, upstream_key)
        
    print(f"[PROXY LOG] === Request Successfully Completed ===")
    
    prompt_tokens = 20000
    completion_tokens = 40000
    total_tokens = prompt_tokens + completion_tokens

    return {
        "id": f"chatcmpl-{int(time.time())}",
        "object": "chat.completion",
        "created": int(time.time()),
        "model": request.model,
        "choices": [
            {
                "index": 0, 
                "message": {
                    "role": "assistant", 
                    "content": final_url
                }, 
                "finish_reason": "stop"
            }
        ],
        "usage": {
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "total_tokens": total_tokens
        }
    }

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=False)