bahi-bh commited on
Commit
53db6f0
Β·
verified Β·
1 Parent(s): 3311f77

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +482 -227
app.py CHANGED
@@ -1,39 +1,316 @@
1
- from fastapi import FastAPI
2
- from fastapi.middleware.cors import CORSMiddleware
3
- from fastapi.responses import StreamingResponse, JSONResponse
4
- from pydantic import BaseModel
5
- from typing import List, Optional
 
 
 
 
 
 
6
 
7
  import asyncio
8
  import json
9
- import uuid
10
- import time
11
  import logging
 
 
 
 
 
 
 
 
 
12
 
13
  import g4f
14
- from g4f.client import Client
 
15
 
16
- # =====================================================
17
  # LOGGING
18
- # =====================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- logging.basicConfig(level=logging.INFO)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- logger = logging.getLogger(__name__)
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- # =====================================================
25
- # APP
26
- # =====================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  app = FastAPI(
29
- title="DuckAI Gateway",
30
- version="6.0.0"
 
31
  )
32
 
33
- # =====================================================
34
- # CORS
35
- # =====================================================
36
-
37
  app.add_middleware(
38
  CORSMiddleware,
39
  allow_origins=["*"],
@@ -42,259 +319,237 @@ app.add_middleware(
42
  allow_headers=["*"],
43
  )
44
 
45
- # =====================================================
46
- # MODELS
47
- # =====================================================
48
 
49
  class Message(BaseModel):
50
  role: str
51
  content: str
52
 
 
53
  class ChatRequest(BaseModel):
54
- model: str
55
  messages: List[Message]
56
- stream: Optional[bool] = False
57
  temperature: Optional[float] = 0.7
58
  max_tokens: Optional[int] = 4096
59
 
60
- # =====================================================
61
- # CLIENT
62
- # =====================================================
63
-
64
- client = Client()
65
-
66
- # =====================================================
67
- # ROOT
68
- # =====================================================
69
-
70
- @app.get("/")
71
- async def root():
72
 
73
- return {
74
- "status": "online",
75
- "service": "DuckAI Gateway"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- # =====================================================
79
- # MODELS
80
- # =====================================================
81
-
82
- @app.get("/v1/models")
83
- async def models():
84
-
85
- output = []
86
-
87
- try:
88
-
89
- all_models = list(g4f.models._all_models)
90
-
91
- for model in all_models:
92
-
93
- output.append({
94
- "id": str(model),
95
- "object": "model",
96
- "created": int(time.time()),
97
- "owned_by": "g4f"
98
- })
99
-
100
- except Exception:
101
-
102
- fallback = [
103
- "gpt-4o-mini",
104
- "gpt-4o",
105
- "gpt-4",
106
- "claude-3-haiku",
107
- "gemini-pro"
108
- ]
109
-
110
- for model in fallback:
111
-
112
- output.append({
113
- "id": model,
114
- "object": "model",
115
- "created": int(time.time()),
116
- "owned_by": "g4f"
117
- })
118
 
 
119
  return {
120
- "object": "list",
121
- "data": output
 
 
 
 
 
 
 
 
 
 
 
 
122
  }
123
 
124
- # =====================================================
125
- # CHAT
126
- # =====================================================
127
 
128
- @app.post("/v1/chat/completions")
129
- async def chat(body: ChatRequest):
 
 
130
 
131
- messages = [
132
- {
133
- "role": m.role,
134
- "content": m.content
135
- }
136
- for m in body.messages
137
- ]
138
 
139
- # =================================================
140
- # STREAM
141
- # =================================================
142
 
143
- if body.stream:
 
 
 
 
 
144
 
145
- async def event_stream():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
 
 
 
 
 
 
 
 
147
  try:
 
 
 
 
 
 
148
 
149
- response = await asyncio.wait_for(
150
- asyncio.to_thread(
151
- client.chat.completions.create,
152
- model=body.model,
153
- messages=messages,
154
- stream=True
155
- ),
156
- timeout=60
157
- )
158
-
159
- chunk_id = f"chatcmpl-{uuid.uuid4().hex}"
160
-
161
- for chunk in response:
162
-
163
- try:
164
-
165
- content = ""
166
-
167
- if (
168
- hasattr(chunk, "choices")
169
- and chunk.choices
170
- and chunk.choices[0].delta
171
- ):
172
- content = chunk.choices[0].delta.content
173
 
174
- if content:
175
 
176
- payload = {
177
- "id": chunk_id,
178
- "object": "chat.completion.chunk",
179
- "created": int(time.time()),
180
- "model": body.model,
181
- "choices": [
182
- {
183
- "index": 0,
184
- "delta": {
185
- "content": content
186
- },
187
- "finish_reason": None
188
- }
189
- ]
190
- }
191
 
192
- yield f"data: {json.dumps(payload)}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
- await asyncio.sleep(0)
195
 
196
- except Exception as e:
197
- logger.error(e)
 
198
 
199
- done_payload = {
200
- "id": chunk_id,
201
- "object": "chat.completion.chunk",
202
- "created": int(time.time()),
203
- "model": body.model,
204
- "choices": [
205
- {
206
- "index": 0,
207
- "delta": {},
208
- "finish_reason": "stop"
209
- }
210
- ]
211
- }
212
 
213
- yield f"data: {json.dumps(done_payload)}\n\n"
214
 
215
- yield "data: [DONE]\n\n"
 
 
 
 
 
216
 
217
- except Exception as e:
218
 
219
- logger.error(e)
 
 
220
 
221
- payload = {
222
- "error": {
223
- "message": str(e)
224
- }
225
- }
226
 
227
- yield f"data: {json.dumps(payload)}\n\n"
 
 
 
228
 
 
 
229
  return StreamingResponse(
230
- event_stream(),
231
  media_type="text/event-stream",
232
  headers={
233
- "Cache-Control": "no-cache",
234
- "Connection": "keep-alive"
235
- }
 
236
  )
237
 
238
- # =================================================
239
- # NORMAL
240
- # =================================================
241
-
242
  try:
243
-
244
- response = await asyncio.wait_for(
245
- asyncio.to_thread(
246
- client.chat.completions.create,
247
- model=body.model,
248
- messages=messages
249
- ),
250
- timeout=60
251
- )
252
-
253
- text = ""
254
-
255
- try:
256
- text = response.choices[0].message.content
257
- except:
258
- text = str(response)
259
-
260
- return JSONResponse({
261
- "id": f"chatcmpl-{uuid.uuid4().hex}",
262
- "object": "chat.completion",
263
- "created": int(time.time()),
264
- "model": body.model,
265
- "choices": [
266
- {
267
- "index": 0,
268
- "message": {
269
- "role": "assistant",
270
- "content": text
271
- },
272
- "finish_reason": "stop"
273
- }
274
- ]
275
- })
276
-
277
  except Exception as e:
 
 
278
 
279
- logger.error(e)
280
 
281
- return JSONResponse(
282
- status_code=500,
283
- content={
284
- "error": str(e)
285
- }
286
- )
287
-
288
- # =====================================================
289
- # RUN
290
- # =====================================================
291
 
292
  if __name__ == "__main__":
293
-
294
  import uvicorn
295
-
296
- uvicorn.run(
297
- app,
298
- host="0.0.0.0",
299
- port=7860
300
- )
 
1
+ """
2
+ Universal AI Gateway v5.0
3
+ ━━━━━━━━━━━━━━━━━━━━━━━━━
4
+ - OpenAI-compatible API
5
+ - g4f multi-provider routing
6
+ - Duck.ai integration (GPT-4o-mini, Claude, Llama, Mixtral)
7
+ - Auto-fallback between providers
8
+ - Streaming + non-streaming
9
+ """
10
+
11
+ from __future__ import annotations
12
 
13
  import asyncio
14
  import json
 
 
15
  import logging
16
+ import time
17
+ import uuid
18
+ from typing import AsyncIterator, List, Optional, Dict, Any
19
+
20
+ import aiohttp
21
+ from fastapi import FastAPI, HTTPException, Request
22
+ from fastapi.middleware.cors import CORSMiddleware
23
+ from fastapi.responses import JSONResponse, StreamingResponse
24
+ from pydantic import BaseModel
25
 
26
  import g4f
27
+ from g4f.client import AsyncClient
28
+ from g4f import Provider
29
 
30
+ # ─────────────────────────────────────────────────────────────────────────────
31
  # LOGGING
32
+ # ─────────────────────────────────────────────────────────────────────────────
33
+
34
+ logging.basicConfig(
35
+ level=logging.INFO,
36
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
37
+ )
38
+ logger = logging.getLogger("ai-gateway")
39
+
40
+ # ─────────────────────────────────────────────────────────────────────────────
41
+ # CONFIG
42
+ # ─────────────────────────────────────────────────────────────────────────────
43
+
44
+ API_KEY = "sk-your-secret-key"
45
+
46
+ # ─────────────────────────────────────────────────────────────────────────────
47
+ # DUCK.AI INTEGRATION
48
+ # Direct integration with https://duck.ai (DuckDuckGo AI Chat)
49
+ # Supported models: gpt-4o-mini, claude-3-haiku, llama-3.3-70b, mixtral-8x7b, o4-mini
50
+ # ─────────────────────────────────────────────────────────────────────────────
51
+
52
+ DUCK_MODEL_MAP: Dict[str, str] = {
53
+ # Public names β†’ Duck.ai internal model IDs
54
+ "gpt-4o-mini": "gpt-4o-mini",
55
+ "claude-3-haiku": "claude-3-haiku-20240307",
56
+ "claude-haiku": "claude-3-haiku-20240307",
57
+ "llama-3.3-70b": "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
58
+ "llama-3.1-70b": "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
59
+ "llama": "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
60
+ "mixtral-8x7b": "mistralai/Mixtral-8x7B-Instruct-v0.1",
61
+ "mixtral": "mistralai/Mixtral-8x7B-Instruct-v0.1",
62
+ "o4-mini": "o4-mini",
63
+ "o3-mini": "o4-mini",
64
+ }
65
+
66
+ DUCK_MODELS_LIST = list(dict.fromkeys(DUCK_MODEL_MAP.keys()))
67
+
68
+ DUCK_VQD_URL = "https://duckduckgo.com/duckchat/v1/status"
69
+ DUCK_CHAT_URL = "https://duckduckgo.com/duckchat/v1/chat"
70
+
71
+ DUCK_HEADERS_BASE = {
72
+ "User-Agent": (
73
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
74
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
75
+ "Chrome/124.0.0.0 Safari/537.36"
76
+ ),
77
+ "Accept": "text/event-stream",
78
+ "Accept-Language": "en-US,en;q=0.9",
79
+ "Referer": "https://duckduckgo.com/",
80
+ "Origin": "https://duckduckgo.com",
81
+ }
82
+
83
+
84
+ async def duck_get_vqd(model_id: str, session: aiohttp.ClientSession) -> str:
85
+ """Fetch the x-vqd-4 token required by Duck.ai."""
86
+ headers = {**DUCK_HEADERS_BASE, "x-vqd-accept": "1"}
87
+ async with session.get(DUCK_VQD_URL, headers=headers) as resp:
88
+ if resp.status != 200:
89
+ raise RuntimeError(f"Duck.ai VQD fetch failed: HTTP {resp.status}")
90
+ vqd = resp.headers.get("x-vqd-4", "")
91
+ if not vqd:
92
+ raise RuntimeError("Duck.ai did not return x-vqd-4 token")
93
+ return vqd
94
+
95
+
96
+ async def duck_chat_stream(
97
+ messages: List[Dict[str, str]],
98
+ model: str,
99
+ ) -> AsyncIterator[str]:
100
+ """
101
+ Async generator that yields text chunks from Duck.ai.
102
+ Handles VQD token refresh automatically.
103
+ """
104
+ duck_model = DUCK_MODEL_MAP.get(model, "gpt-4o-mini")
105
+
106
+ async with aiohttp.ClientSession() as session:
107
+ vqd = await duck_get_vqd(duck_model, session)
108
+
109
+ payload = {
110
+ "model": duck_model,
111
+ "messages": messages,
112
+ }
113
+
114
+ headers = {
115
+ **DUCK_HEADERS_BASE,
116
+ "Content-Type": "application/json",
117
+ "x-vqd-4": vqd,
118
+ }
119
+
120
+ async with session.post(
121
+ DUCK_CHAT_URL,
122
+ json=payload,
123
+ headers=headers,
124
+ ) as resp:
125
+ if resp.status != 200:
126
+ body = await resp.text()
127
+ raise RuntimeError(
128
+ f"Duck.ai chat failed: HTTP {resp.status} β€” {body[:200]}"
129
+ )
130
 
131
+ async for raw_line in resp.content:
132
+ line = raw_line.decode("utf-8", errors="replace").strip()
133
+ if not line or not line.startswith("data:"):
134
+ continue
135
+ data_str = line[len("data:"):].strip()
136
+ if data_str == "[DONE]":
137
+ break
138
+ try:
139
+ data = json.loads(data_str)
140
+ chunk = data.get("message", "")
141
+ if chunk:
142
+ yield chunk
143
+ except json.JSONDecodeError:
144
+ continue
145
+
146
+
147
+ async def duck_chat_complete(
148
+ messages: List[Dict[str, str]],
149
+ model: str,
150
+ ) -> str:
151
+ """Collect full Duck.ai response (non-streaming)."""
152
+ parts: List[str] = []
153
+ async for chunk in duck_chat_stream(messages, model):
154
+ parts.append(chunk)
155
+ return "".join(parts)
156
+
157
+
158
+ # ─────────────────────────────────────────────────────────────────────────────
159
+ # G4F PROVIDER ROUTING
160
+ # Maps model names β†’ preferred g4f provider with fallback chain
161
+ # ─────────────────────────────────────────────────────────────────────────────
162
+
163
+ G4F_PROVIDER_MAP: Dict[str, Any] = {
164
+ # OpenAI GPT family
165
+ "gpt-4": Provider.OpenaiChat,
166
+ "gpt-4o": Provider.OpenaiChat,
167
+ "gpt-4o-mini": Provider.OpenaiChat,
168
+ "gpt-4.1": Provider.OpenaiChat,
169
+ "gpt-4.1-mini": Provider.OpenaiChat,
170
+ "gpt-5": Provider.OpenaiChat,
171
+ "auto": Provider.OpenaiChat,
172
+
173
+ # Anthropic Claude
174
+ "claude-3-haiku": Provider.Anthropic,
175
+ "claude-3-sonnet": Provider.Anthropic,
176
+ "claude-3-opus": Provider.Anthropic,
177
+ "claude-3-5-sonnet": Provider.Anthropic,
178
+ "claude-3-7-sonnet": Provider.Anthropic,
179
+ "claude-sonnet-4": Provider.Anthropic,
180
+
181
+ # Google Gemini
182
+ "gemini-pro": Provider.GeminiPro,
183
+ "gemini-1.5-pro": Provider.GeminiPro,
184
+ "gemini-2.5-flash": Provider.Gemini,
185
+ "gemini-2.5-pro": Provider.Gemini,
186
+
187
+ # Meta Llama
188
+ "llama-3.1-70b": Provider.Cerebras,
189
+ "llama-3.3-70b": Provider.Cerebras,
190
+ "llama-3.1-8b": Provider.Cerebras,
191
+
192
+ # Mistral / Mixtral
193
+ "mixtral-8x7b": Provider.HuggingFace,
194
+ "mistral-7b": Provider.HuggingFace,
195
+
196
+ # DeepSeek
197
+ "deepseek-chat": Provider.DeepSeek,
198
+ "deepseek-r1": Provider.DeepSeek,
199
+
200
+ # Qwen
201
+ "qwen-2.5": Provider.Qwen,
202
+ "qwen-3": Provider.Qwen,
203
+ "qwen-2-72b": Provider.Qwen,
204
+
205
+ # Grok
206
+ "grok-3": Provider.Grok,
207
+ "grok-4": Provider.Grok,
208
+
209
+ # Copilot
210
+ "copilot": Provider.Copilot,
211
+ "o1": Provider.Copilot,
212
+ "o3-mini": Provider.Copilot,
213
+ "o4-mini": Provider.Copilot,
214
+
215
+ # Blackbox (multi-model)
216
+ "blackbox": Provider.BlackboxPro,
217
+ "openai/gpt-5": Provider.BlackboxPro,
218
+ "x-ai/grok-4": Provider.BlackboxPro,
219
+
220
+ # Perplexity
221
+ "perplexity": Provider.Perplexity,
222
+ "perplexity-turbo": Provider.Perplexity,
223
+
224
+ # Pi
225
+ "pi": Provider.Pi,
226
+ }
227
+
228
+
229
+ def get_g4f_provider(model: str) -> Optional[Any]:
230
+ """Return the best g4f provider for a model name."""
231
+ # Exact match
232
+ if model in G4F_PROVIDER_MAP:
233
+ return G4F_PROVIDER_MAP[model]
234
+ # Prefix match (e.g. "gpt-4o-mini-2024" β†’ gpt-4o-mini)
235
+ for key, provider in G4F_PROVIDER_MAP.items():
236
+ if model.startswith(key) or key.startswith(model):
237
+ return provider
238
+ return None
239
+
240
+
241
+ # ─────────────────────────────────────────────────────────────────────────────
242
+ # MODELS REGISTRY
243
+ # ─────────────────────────────────────────────────────────────────────────────
244
+
245
+ def build_models_list() -> List[Dict]:
246
+ """Build the /v1/models response combining Duck.ai + g4f providers."""
247
+ now = int(time.time())
248
+ seen: set = set()
249
+ models: List[Dict] = []
250
+
251
+ # Duck.ai models
252
+ for m in DUCK_MODELS_LIST:
253
+ if m not in seen:
254
+ seen.add(m)
255
+ models.append({
256
+ "id": m,
257
+ "object": "model",
258
+ "created": now,
259
+ "owned_by": "duck-ai",
260
+ "provider": "duck.ai",
261
+ })
262
 
263
+ # g4f provider models
264
+ for model_name, provider in G4F_PROVIDER_MAP.items():
265
+ if model_name not in seen:
266
+ seen.add(model_name)
267
+ pname = getattr(provider, "__name__", str(provider))
268
+ models.append({
269
+ "id": model_name,
270
+ "object": "model",
271
+ "created": now,
272
+ "owned_by": "g4f",
273
+ "provider": pname,
274
+ })
275
 
276
+ # Extra g4f working providers
277
+ for pname in dir(Provider):
278
+ if pname.startswith("_") or pname[0].islower():
279
+ continue
280
+ try:
281
+ p = getattr(Provider, pname)
282
+ if not (hasattr(p, "working") and p.working):
283
+ continue
284
+ pmodels = getattr(p, "models", None)
285
+ if not pmodels or callable(pmodels):
286
+ continue
287
+ for m in pmodels:
288
+ m = str(m)
289
+ if m and m not in seen:
290
+ seen.add(m)
291
+ models.append({
292
+ "id": m,
293
+ "object": "model",
294
+ "created": now,
295
+ "owned_by": "g4f",
296
+ "provider": pname,
297
+ })
298
+ except Exception:
299
+ continue
300
+
301
+ return models
302
+
303
+
304
+ # ─────────────────────────────────────────────────────────────────────────────
305
+ # FASTAPI APP
306
+ # ─────────────────────────────────────────────────────────────────────────────
307
 
308
  app = FastAPI(
309
+ title="Universal AI Gateway",
310
+ version="5.0.0",
311
+ description="OpenAI-compatible gateway with Duck.ai + g4f multi-provider support",
312
  )
313
 
 
 
 
 
314
  app.add_middleware(
315
  CORSMiddleware,
316
  allow_origins=["*"],
 
319
  allow_headers=["*"],
320
  )
321
 
322
+ # ─────────────────────────────────────────────────────────────────────────────
323
+ # SCHEMAS
324
+ # ─────────────────────────────────────────────────────────────────────────────
325
 
326
  class Message(BaseModel):
327
  role: str
328
  content: str
329
 
330
+
331
  class ChatRequest(BaseModel):
332
+ model: str = "gpt-4o-mini"
333
  messages: List[Message]
334
+ stream: bool = False
335
  temperature: Optional[float] = 0.7
336
  max_tokens: Optional[int] = 4096
337
 
 
 
 
 
 
 
 
 
 
 
 
 
338
 
339
+ # ─────────────────────────────────────────────────────────────────────────────
340
+ # AUTH
341
+ # ─────────────────────────────────────────────────────────────────────────────
342
+
343
+ def verify_api_key(req: Request) -> bool:
344
+ auth = req.headers.get("Authorization", "")
345
+ if not auth:
346
+ return True # Allow unauthenticated for testing
347
+ if not auth.startswith("Bearer "):
348
+ raise HTTPException(status_code=401, detail="Invalid Authorization format")
349
+ token = auth.removeprefix("Bearer ").strip()
350
+ if token != API_KEY:
351
+ raise HTTPException(status_code=403, detail="Invalid API key")
352
+ return True
353
+
354
+
355
+ # ─────────────────────────────────────────────────────────────────────────────
356
+ # SSE HELPERS
357
+ # ─────────────────────────────────────────────────────────────────────────────
358
+
359
+ def sse_chunk(chunk_id: str, model: str, content: str) -> str:
360
+ payload = {
361
+ "id": chunk_id,
362
+ "object": "chat.completion.chunk",
363
+ "created": int(time.time()),
364
+ "model": model,
365
+ "choices": [{
366
+ "index": 0,
367
+ "delta": {"content": content},
368
+ "finish_reason": None,
369
+ }],
370
  }
371
+ return f"data: {json.dumps(payload)}\n\n"
372
+
373
+
374
+ def sse_done(chunk_id: str, model: str) -> str:
375
+ payload = {
376
+ "id": chunk_id,
377
+ "object": "chat.completion.chunk",
378
+ "created": int(time.time()),
379
+ "model": model,
380
+ "choices": [{
381
+ "index": 0,
382
+ "delta": {},
383
+ "finish_reason": "stop",
384
+ }],
385
+ }
386
+ return f"data: {json.dumps(payload)}\n\ndata: [DONE]\n\n"
387
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
 
389
+ def build_response(chunk_id: str, model: str, content: str) -> Dict:
390
  return {
391
+ "id": chunk_id,
392
+ "object": "chat.completion",
393
+ "created": int(time.time()),
394
+ "model": model,
395
+ "choices": [{
396
+ "index": 0,
397
+ "message": {"role": "assistant", "content": content},
398
+ "finish_reason": "stop",
399
+ }],
400
+ "usage": {
401
+ "prompt_tokens": 0,
402
+ "completion_tokens": 0,
403
+ "total_tokens": 0,
404
+ },
405
  }
406
 
 
 
 
407
 
408
+ # ─────────────────────────────────────────────────────────────────────────────
409
+ # CORE ROUTING LOGIC
410
+ # Priority: Duck.ai β†’ specific g4f provider β†’ g4f auto-routing β†’ error
411
+ # ─────────────────────────────────────────────────────────────────────────────
412
 
413
+ def is_duck_model(model: str) -> bool:
414
+ return model in DUCK_MODEL_MAP
 
 
 
 
 
415
 
 
 
 
416
 
417
+ async def route_stream(
418
+ messages: List[Dict],
419
+ model: str,
420
+ chunk_id: str,
421
+ ) -> AsyncIterator[str]:
422
+ """Unified streaming router."""
423
 
424
+ # ── Duck.ai ──────────────────────────────────────────────────────────────
425
+ if is_duck_model(model):
426
+ logger.info(f"[STREAM] Duck.ai β†’ model={model}")
427
+ try:
428
+ async for chunk in duck_chat_stream(messages, model):
429
+ yield sse_chunk(chunk_id, model, chunk)
430
+ yield sse_done(chunk_id, model)
431
+ return
432
+ except Exception as e:
433
+ logger.warning(f"[STREAM] Duck.ai failed ({e}), trying g4f fallback")
434
+
435
+ # ── g4f provider ─────────────────────────────────────────────────────────
436
+ provider = get_g4f_provider(model)
437
+ provider_name = getattr(provider, "__name__", "auto") if provider else "auto"
438
+ logger.info(f"[STREAM] g4f provider={provider_name} model={model}")
439
 
440
+ try:
441
+ client = AsyncClient(provider=provider)
442
+ response = await client.chat.completions.create(
443
+ model=model,
444
+ messages=messages,
445
+ stream=True,
446
+ )
447
+ async for chunk in response:
448
  try:
449
+ content = chunk.choices[0].delta.content or ""
450
+ if content:
451
+ yield sse_chunk(chunk_id, model, content)
452
+ except Exception:
453
+ continue
454
+ yield sse_done(chunk_id, model)
455
 
456
+ except Exception as e:
457
+ logger.error(f"[STREAM] g4f error: {e}")
458
+ error_payload = {"error": {"message": str(e), "type": "server_error"}}
459
+ yield f"data: {json.dumps(error_payload)}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
 
 
461
 
462
+ async def route_complete(
463
+ messages: List[Dict],
464
+ model: str,
465
+ ) -> str:
466
+ """Unified non-streaming router."""
 
 
 
 
 
 
 
 
 
 
467
 
468
+ # ── Duck.ai ──────────────────────────────────────────────────────────────
469
+ if is_duck_model(model):
470
+ logger.info(f"[COMPLETE] Duck.ai β†’ model={model}")
471
+ try:
472
+ return await duck_chat_complete(messages, model)
473
+ except Exception as e:
474
+ logger.warning(f"[COMPLETE] Duck.ai failed ({e}), trying g4f fallback")
475
+
476
+ # ── g4f provider ─────────────────────────────────────────────────────────
477
+ provider = get_g4f_provider(model)
478
+ provider_name = getattr(provider, "__name__", "auto") if provider else "auto"
479
+ logger.info(f"[COMPLETE] g4f provider={provider_name} model={model}")
480
+
481
+ client = AsyncClient(provider=provider)
482
+ response = await client.chat.completions.create(
483
+ model=model,
484
+ messages=messages,
485
+ stream=False,
486
+ )
487
+ try:
488
+ return response.choices[0].message.content or ""
489
+ except Exception:
490
+ return str(response)
491
 
 
492
 
493
+ # ─────────────────────────────────────────────────────────────────────────────
494
+ # ROUTES
495
+ # ─────────────────────────────────────────────────────────────────────────────
496
 
497
+ @app.get("/")
498
+ async def root():
499
+ return {
500
+ "status": "online",
501
+ "service": "Universal AI Gateway",
502
+ "version": "5.0.0",
503
+ "docs": "/docs",
504
+ "models": "/v1/models",
505
+ }
 
 
 
 
506
 
 
507
 
508
+ @app.get("/v1/models")
509
+ async def get_models():
510
+ return {
511
+ "object": "list",
512
+ "data": build_models_list(),
513
+ }
514
 
 
515
 
516
+ @app.post("/v1/chat/completions")
517
+ async def chat_completions(req: Request, body: ChatRequest):
518
+ verify_api_key(req)
519
 
520
+ messages = [{"role": m.role, "content": m.content} for m in body.messages]
521
+ chunk_id = f"chatcmpl-{uuid.uuid4().hex}"
 
 
 
522
 
523
+ logger.info(
524
+ f"Request: model={body.model!r} stream={body.stream} "
525
+ f"messages={len(messages)}"
526
+ )
527
 
528
+ # ── Streaming ─────────────────────────────────────────────────────────
529
+ if body.stream:
530
  return StreamingResponse(
531
+ route_stream(messages, body.model, chunk_id),
532
  media_type="text/event-stream",
533
  headers={
534
+ "Cache-Control": "no-cache",
535
+ "Connection": "keep-alive",
536
+ "X-Accel-Buffering": "no",
537
+ },
538
  )
539
 
540
+ # ── Non-streaming ─────────────────────────────────────────────────────
 
 
 
541
  try:
542
+ content = await route_complete(messages, body.model)
543
+ return JSONResponse(build_response(chunk_id, body.model, content))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
  except Exception as e:
545
+ logger.error(f"Chat error: {e}")
546
+ raise HTTPException(status_code=500, detail=str(e))
547
 
 
548
 
549
+ # ─────────────────────────────────────────────────────────────────────────────
550
+ # ENTRY POINT
551
+ # ─────────────────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
552
 
553
  if __name__ == "__main__":
 
554
  import uvicorn
555
+ uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info")