| from fastapi import FastAPI, HTTPException, Request |
| from fastapi.responses import JSONResponse, StreamingResponse |
| from fastapi.middleware.cors import CORSMiddleware |
| import requests |
| import json |
| import os |
| import time |
| import asyncio |
|
|
| app = FastAPI() |
|
|
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| |
| STATUS_URL = os.environ.get("STATUS_URL", "https://duckduckgo.com/duckchat/v1/status") |
| CHAT_URL = os.environ.get("CHAT_URL", "https://duckduckgo.com/duckchat/v1/chat") |
| REFERER = os.environ.get("REFERER", "https://duckduckgo.com/") |
| ORIGIN = os.environ.get("ORIGIN", "https://duckduckgo.com") |
| USER_AGENT = os.environ.get("USER_AGENT", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36") |
| COOKIE = os.environ.get("COOKIE", "dcm=3; s=l; bf=1") |
|
|
| DEFAULT_HEADERS = { |
| "User-Agent": USER_AGENT, |
| "Accept": "text/event-stream", |
| "Accept-Language": "en-US,en;q=0.5", |
| "Referer": REFERER, |
| "Content-Type": "application/json", |
| "Origin": ORIGIN, |
| "Connection": "keep-alive", |
| "Cookie": COOKIE, |
| "Sec-Fetch-Dest": "empty", |
| "Sec-Fetch-Mode": "cors", |
| "Sec-Fetch-Site": "same-origin", |
| "Pragma": "no-cache", |
| "TE": "trailers", |
| } |
|
|
| SUPPORTED_MODELS = [ |
| "o3-mini", |
| "gpt-4o-mini", |
| "claude-3-haiku-20240307", |
| "meta-llama/Llama-3.3-70B-Instruct-Turbo", |
| ] |
|
|
| async def get_vqd(): |
| """Get the VQD value for DuckDuckGo Chat.""" |
| headers = {**DEFAULT_HEADERS, "x-vqd-accept": "1"} |
| try: |
| response = requests.get(STATUS_URL, headers=headers) |
| response.raise_for_status() |
| vqd = response.headers.get("x-vqd-4") |
| if not vqd: |
| raise ValueError("x-vqd-4 header not found in the response.") |
| return vqd |
| except requests.exceptions.RequestException as e: |
| raise HTTPException(status_code=500, detail=f"HTTP request failed: {e}") |
| except ValueError as e: |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| async def duckduckgo_chat_stream(model, messages): |
| """Interact with DuckDuckGo Chat with streaming output.""" |
| try: |
| x_vqd_4 = await get_vqd() |
|
|
| chat_headers = { |
| **DEFAULT_HEADERS, |
| "x-vqd-4": x_vqd_4, |
| "Accept": "text/event-stream", |
| } |
|
|
| body = json.dumps({ |
| "model": model, |
| "messages": messages, |
| }) |
|
|
| response = requests.post(CHAT_URL, headers=chat_headers, data=body, stream=True) |
| response.raise_for_status() |
|
|
| async def event_stream(): |
| try: |
| for line in response.iter_lines(): |
| if line: |
| decoded_line = line.decode('utf-8') |
| if decoded_line.startswith("data: "): |
| content = decoded_line[5:].strip() |
| |
| if content == "[DONE]": |
| yield f"data: [DONE]\n\n" |
| break |
| try: |
| json_data = json.loads(content) |
| message_content = json_data.get("message", "") |
| if message_content: |
| |
| openai_stream_response = { |
| "id": f"chatcmpl-{int(time.time() * 1000)}", |
| "object": "chat.completion.chunk", |
| "created": int(time.time()), |
| "model": model, |
| "choices": [ |
| { |
| "delta": {"content": message_content}, |
| "index": 0, |
| "finish_reason": None, |
| } |
| ], |
| } |
| yield f"data: {json.dumps(openai_stream_response)}\n\n" |
| await asyncio.sleep(0.01) |
| except json.JSONDecodeError as e: |
| print(f"JSON decode error: {e}, line: {decoded_line}") |
| yield f"data: {json.dumps({'error': 'JSON decode error'})}\n\n" |
| break |
| except requests.exceptions.RequestException as e: |
| print(f"Request error: {e}") |
| yield f"data: {json.dumps({'error': 'Request error'})}\n\n" |
| except Exception as e: |
| print(f"An error occurred: {e}") |
| yield f"data: {json.dumps({'error': 'An error occurred'})}\n\n" |
| finally: |
| yield "data: [DONE]\n\n" |
|
|
| return StreamingResponse(event_stream(), media_type="text/event-stream") |
|
|
| except requests.exceptions.RequestException as e: |
| raise HTTPException(status_code=500, detail=f"HTTP request failed: {e}") |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"Error during chat: {e}") |
|
|
| async def duckduckgo_chat_non_stream(model, messages): |
| """Interact with DuckDuckGo Chat without streaming output.""" |
| try: |
| x_vqd_4 = await get_vqd() |
|
|
| chat_headers = { |
| **DEFAULT_HEADERS, |
| "x-vqd-4": x_vqd_4, |
| } |
|
|
| body = json.dumps({ |
| "model": model, |
| "messages": messages, |
| }) |
|
|
| response = requests.post(CHAT_URL, headers=chat_headers, data=body) |
| response.raise_for_status() |
|
|
| full_message = "" |
| for line in response.iter_lines(): |
| if line: |
| decoded_line = line.decode('utf-8') |
| if decoded_line.startswith("data: "): |
| try: |
| json_data = json.loads(decoded_line[5:]) |
| full_message += json_data.get("message", "") |
| except json.JSONDecodeError as e: |
| print(f"JSON decode error: {e}, line: {decoded_line}") |
| pass |
|
|
| return full_message |
|
|
| except requests.exceptions.RequestException as e: |
| raise HTTPException(status_code=500, detail=f"HTTP request failed: {e}") |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"Error during chat: {e}") |
|
|
| @app.post("/v1/chat/completions") |
| async def chat_completions(request: Request): |
| try: |
| body = await request.json() |
| if not body: |
| raise HTTPException(status_code=400, detail="Invalid request body") |
|
|
| model = body.get("model", "o3-mini") |
| if model not in SUPPORTED_MODELS: |
| raise HTTPException( |
| status_code=400, |
| detail=f"Model \"{model}\" is not supported. Supported models are: {', '.join(SUPPORTED_MODELS)}." |
| ) |
|
|
| messages = body.get("messages") |
| if not messages: |
| raise HTTPException(status_code=400, detail="No message content provided") |
|
|
| stream = body.get("stream", False) |
|
|
| |
| system_message = next((msg for msg in messages if msg.get("role") == "system"), None) |
| system_prompt = f"You will play the role of a {system_message['content']}.\n" if system_message else "" |
|
|
| |
| history_messages = "\n".join( |
| f"{msg['role']}: {msg['content']}" |
| for msg in messages |
| if msg.get("role") != "system" and msg != messages[-1] |
| ) |
|
|
| |
| last_user_message = messages[-1] |
| current_question = last_user_message["content"] if last_user_message.get("role") == "user" else "" |
|
|
| |
| combined_message_content = ( |
| f"{system_prompt}Below is the conversation history:\n{history_messages}\n" |
| f"User's current question: {current_question}" |
| ) |
| combined_message = {"role": "user", "content": combined_message_content} |
|
|
| if stream: |
| return await duckduckgo_chat_stream(model, [combined_message]) |
| else: |
| response_text = await duckduckgo_chat_non_stream(model, [combined_message]) |
|
|
| |
| openai_response = { |
| "id": f"chatcmpl-{int(time.time() * 1000)}", |
| "object": "chat.completion", |
| "created": int(time.time()), |
| "model": model, |
| "choices": [ |
| { |
| "message": { |
| "role": "assistant", |
| "content": response_text, |
| }, |
| "finish_reason": "stop", |
| "index": 0, |
| }, |
| ], |
| "usage": { |
| "prompt_tokens": 0, |
| "completion_tokens": 0, |
| "total_tokens": 0 |
| }, |
| } |
|
|
| return JSONResponse(content=openai_response) |
|
|
| except HTTPException as e: |
| raise e |
| except Exception as e: |
| print(f"API error: {e}") |
| raise HTTPException(status_code=500, detail=f"Internal server error: {e}") |
|
|
| @app.exception_handler(HTTPException) |
| async def http_exception_handler(request: Request, exc: HTTPException): |
| return JSONResponse( |
| status_code=exc.status_code, |
| content={"detail": exc.detail}, |
| ) |
|
|
| @app.get("/") |
| async def greet_json(): |
| return {"Hello": "World!"} |
|
|