Guest User

Untitled

a guest
Dec 28th, 2025
32
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.37 KB | None | 0 0
  1. import os
  2. import json
  3. import time
  4. import tomllib
  5. import logging
  6. import asyncio
  7. import uuid
  8. import httpx
  9. from fastapi import FastAPI, Request, HTTPException
  10. from fastapi.responses import StreamingResponse, JSONResponse
  11. from fastapi.middleware.cors import CORSMiddleware
  12. from typing import List, Dict, Any, Optional
  13.  
  14. # Setup logging
  15. logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
  16. logger = logging.getLogger(__name__)
  17.  
  18. # Load config
  19. try:
  20. with open("config.toml", "rb") as f:
  21. config = tomllib.load(f)
  22. except FileNotFoundError:
  23. logger.warning("config.toml not found, using defaults")
  24. config = {}
  25.  
  26. general_config = config.get("general", {})
  27.  
  28. PROVIDERS = config.get("providers", [])
  29.  
  30. MODEL_PROVIDER_MAP = {}
  31. ALL_MODELS = []
  32.  
  33. for provider in PROVIDERS:
  34. p_name = provider.get("name", "unknown")
  35. p_models = provider.get("models", [])
  36. p_api_base = provider.get("api_base", "").rstrip("/")
  37.  
  38. # Resolve API Key
  39. p_api_key_env = provider.get("api_key_env")
  40. p_api_key = os.getenv(p_api_key_env) if p_api_key_env else provider.get("api_key")
  41.  
  42. if not p_api_base:
  43. logger.warning(f"Provider {p_name} missing api_base, skipping.")
  44. continue
  45.  
  46. provider_settings = {
  47. "api_base": p_api_base,
  48. "api_key": p_api_key,
  49. "name": p_name
  50. }
  51.  
  52. for m in p_models:
  53. MODEL_PROVIDER_MAP[m] = provider_settings
  54. ALL_MODELS.append(m)
  55.  
  56. DEFAULT_MODEL = ALL_MODELS[0] if ALL_MODELS else "GLM-4.7"
  57.  
  58. app = FastAPI(title="OpenAI-to-Anthropic Raw Proxy")
  59.  
  60. app.add_middleware(
  61. CORSMiddleware,
  62. allow_origins=["*"],
  63. allow_credentials=True,
  64. allow_methods=["*"],
  65. allow_headers=["*"],
  66. )
  67.  
  68. def convert_messages(openai_messages: List[Dict[str, Any]]) -> tuple[Optional[str], List[Dict[str, Any]]]:
  69. """
  70. Extracts system prompt and converts messages to Anthropic format.
  71. """
  72. system_prompt = None
  73. anthropic_messages = []
  74.  
  75. for msg in openai_messages:
  76. role = msg.get("role")
  77. content = msg.get("content")
  78.  
  79. if role == "system":
  80. if system_prompt:
  81. system_prompt += "\n" + content
  82. else:
  83. system_prompt = content
  84. elif role in ["user", "assistant"]:
  85. anthropic_messages.append({"role": role, "content": content})
  86. else:
  87. # Anthropic API only allows system, user, and assistant in messages
  88. logger.warning(f"Skipping unsupported role: {role}")
  89.  
  90. return system_prompt, anthropic_messages
  91.  
  92. @app.get("/v1/models")
  93. async def list_models():
  94. return {
  95. "object": "list",
  96. "data": [
  97. {
  98. "id": model_id,
  99. "object": "model",
  100. "created": int(time.time()),
  101. "owned_by": MODEL_PROVIDER_MAP.get(model_id, {}).get("name", "anthropic-proxy")
  102. }
  103. for model_id in ALL_MODELS
  104. ]
  105. }
  106.  
  107. @app.post("/v1/chat/completions")
  108. async def chat_completions(request: Request):
  109. try:
  110. body = await request.json()
  111. except Exception:
  112. raise HTTPException(status_code=400, detail="Invalid JSON")
  113.  
  114. # Extract parameters
  115. model = body.get("model")
  116. if not model:
  117. model = DEFAULT_MODEL
  118.  
  119. # Find provider
  120. provider = MODEL_PROVIDER_MAP.get(model)
  121. if not provider:
  122. # Strict mapping: only allow explicitly configured models
  123. # Unknown models are rejected since we can't determine which API key to use
  124. raise HTTPException(status_code=404, detail=f"Model '{model}' not configured in proxy.")
  125. else:
  126. raise HTTPException(status_code=500, detail="No providers configured.")
  127.  
  128. target_api_base = provider["api_base"]
  129. target_api_key = provider["api_key"]
  130.  
  131. messages = body.get("messages", [])
  132. stream = body.get("stream", False)
  133. temperature = body.get("temperature")
  134. top_p = body.get("top_p")
  135. stop = body.get("stop")
  136. max_tokens = body.get("max_tokens", 8192)
  137.  
  138. # Convert messages
  139. system_prompt, anthropic_messages = convert_messages(messages)
  140.  
  141. # Prepare Anthropic payload
  142. payload = {
  143. "model": model,
  144. "messages": anthropic_messages,
  145. "max_tokens": max_tokens,
  146. "stream": stream,
  147. "thinking": {"type": "enabled", "budget_tokens": 1024}
  148. }
  149.  
  150. if system_prompt:
  151. payload["system"] = system_prompt
  152. if temperature is not None:
  153. payload["temperature"] = temperature
  154. if top_p is not None:
  155. payload["top_p"] = top_p
  156. if stop is not None:
  157. if isinstance(stop, str):
  158. payload["stop_sequences"] = [stop]
  159. else:
  160. payload["stop_sequences"] = stop
  161.  
  162. # Headers
  163. headers = {
  164. "x-api-key": target_api_key,
  165. "anthropic-version": "2023-06-01",
  166. "content-type": "application/json",
  167. "accept": "application/json"
  168. }
  169.  
  170. url = f"{target_api_base}/v1/messages"
  171.  
  172. logger.info(f"Forwarding request to {url} for model {model} (Provider: {provider['name']})")
  173.  
  174. client = httpx.AsyncClient(timeout=60.0)
  175.  
  176. try:
  177. if stream:
  178. req = client.build_request("POST", url, headers=headers, json=payload)
  179. r = await client.send(req, stream=True)
  180.  
  181. if r.status_code != 200:
  182. error_content = await r.aread()
  183. logger.error(f"Target API Error: {r.status_code} - {error_content.decode()}")
  184. await client.aclose()
  185. return JSONResponse(
  186. status_code=r.status_code,
  187. content={"error": {"message": "Upstream error", "details": error_content.decode()}}
  188. )
  189.  
  190. async def sse_generator():
  191. chat_id = f"chatcmpl-{uuid.uuid4()}"
  192. created = int(time.time())
  193.  
  194. try:
  195. async for line in r.aiter_lines():
  196. if not line or not line.startswith("data: "):
  197. continue
  198.  
  199. data_str = line[6:].strip()
  200. if data_str == "[DONE]":
  201. yield "data: [DONE]\n\n"
  202. break
  203.  
  204. try:
  205. event = json.loads(data_str)
  206. event_type = event.get("type")
  207.  
  208. if event_type == "ping":
  209. continue
  210.  
  211. chunk = {
  212. "id": chat_id,
  213. "object": "chat.completion.chunk",
  214. "created": created,
  215. "model": model,
  216. "choices": [
  217. {
  218. "index": 0,
  219. "delta": {},
  220. "finish_reason": None
  221. }
  222. ]
  223. }
  224.  
  225. should_yield = False
  226. if event_type == "content_block_delta":
  227. delta = event.get("delta", {})
  228. delta_type = delta.get("type")
  229.  
  230. if delta_type == "text_delta":
  231. chunk["choices"][0]["delta"]["content"] = delta.get("text", "")
  232. should_yield = True
  233. elif delta_type == "thinking_delta":
  234. chunk["choices"][0]["delta"]["reasoning_content"] = delta.get("thinking", "")
  235. should_yield = True
  236.  
  237. elif event_type == "message_start":
  238. # Initial role
  239. chunk["choices"][0]["delta"]["role"] = "assistant"
  240. should_yield = True
  241.  
  242. elif event_type == "message_delta":
  243. stop_reason = event.get("delta", {}).get("stop_reason")
  244. if stop_reason:
  245. chunk["choices"][0]["delta"] = {} # Empty delta
  246. chunk["choices"][0]["finish_reason"] = stop_reason
  247. should_yield = True
  248.  
  249. elif event_type == "message_stop":
  250. chunk["choices"][0]["delta"] = {}
  251. chunk["choices"][0]["finish_reason"] = "stop"
  252. should_yield = True
  253.  
  254. if should_yield:
  255. yield f"data: {json.dumps(chunk)}\n\n"
  256.  
  257. except json.JSONDecodeError:
  258. logger.warning(f"Failed to parse JSON: {data_str}")
  259. continue
  260. except Exception as inner_e:
  261. logger.error(f"Error processing event {data_str}: {inner_e}", exc_info=True)
  262. raise inner_e
  263. except Exception as e:
  264. logger.error(f"Stream error: {e}")
  265. yield f"data: {{'error': '{str(e)}'}}\n\n"
  266. finally:
  267. await r.aclose()
  268. await client.aclose()
  269.  
  270. return StreamingResponse(sse_generator(), media_type="text/event-stream")
  271.  
  272. else:
  273. response = await client.post(url, headers=headers, json=payload)
  274. await client.aclose()
  275.  
  276. if response.status_code != 200:
  277. return JSONResponse(
  278. status_code=response.status_code,
  279. content={"error": {"message": "Upstream error", "details": response.text}}
  280. )
  281.  
  282. anthropic_resp = response.json()
  283.  
  284. # Map response to OpenAI format
  285. content_blocks = anthropic_resp.get("content", [])
  286. text_content = ""
  287. reasoning_content = ""
  288.  
  289. for block in content_blocks:
  290. if block.get("type") == "text":
  291. text_content += block.get("text", "")
  292. elif block.get("type") == "thinking":
  293. reasoning_content += block.get("thinking", "")
  294.  
  295. openai_resp = {
  296. "id": anthropic_resp.get("id"),
  297. "object": "chat.completion",
  298. "created": int(time.time()),
  299. "model": model,
  300. "choices": [
  301. {
  302. "index": 0,
  303. "message": {
  304. "role": "assistant",
  305. "content": text_content,
  306. },
  307. "finish_reason": anthropic_resp.get("stop_reason")
  308. }
  309. ],
  310. "usage": {
  311. "prompt_tokens": anthropic_resp.get("usage", {}).get("input_tokens", 0),
  312. "completion_tokens": anthropic_resp.get("usage", {}).get("output_tokens", 0),
  313. "total_tokens": anthropic_resp.get("usage", {}).get("input_tokens", 0) + anthropic_resp.get("usage", {}).get("output_tokens", 0)
  314. }
  315. }
  316. if reasoning_content:
  317. openai_resp["choices"][0]["message"]["reasoning_content"] = reasoning_content
  318.  
  319. openai_resp["usage"]["total_tokens"] = openai_resp["usage"]["prompt_tokens"] + openai_resp["usage"]["completion_tokens"]
  320.  
  321. return openai_resp
  322.  
  323. except Exception as e:
  324. await client.aclose()
  325. logger.error(f"Proxy error: {e}")
  326. raise HTTPException(status_code=500, detail=str(e))
  327.  
  328. if __name__ == "__main__":
  329. import uvicorn
  330. port = int(general_config.get("port", 5050))
  331. print(f"Starting raw proxy on port {port}...")
  332. uvicorn.run(app, host="0.0.0.0", port=port)
  333.  
Advertisement
Add Comment
Please, Sign In to add comment