File size: 4,509 Bytes
79fc65b
7ca7fc2
deec013
79fc65b
deec013
7ca7fc2
 
79fc65b
7ca7fc2
79fc65b
7ca7fc2
79fc65b
 
 
 
 
 
 
 
 
 
7ca7fc2
 
 
 
 
79fc65b
 
7ca7fc2
 
 
79fc65b
7ca7fc2
 
 
 
 
 
 
 
 
deec013
7ca7fc2
 
79fc65b
 
 
 
7ca7fc2
 
 
79fc65b
7ca7fc2
 
 
 
 
 
 
 
 
 
79fc65b
7ca7fc2
 
 
 
79fc65b
7ca7fc2
 
 
 
 
79fc65b
7ca7fc2
 
 
 
 
 
79fc65b
 
7ca7fc2
 
79fc65b
7ca7fc2
 
 
 
 
79fc65b
 
 
7ca7fc2
 
 
79fc65b
 
 
 
 
 
 
7ca7fc2
 
79fc65b
 
7ca7fc2
79fc65b
 
7ca7fc2
 
 
79fc65b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import asyncio
import os
import json
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Response
from fastapi.responses import StreamingResponse, JSONResponse
import aiohttp

# ================= 配置区 =================
TARGET_BASE_URL = "https://api.deepinfra.com/v1/openai"
REAL_API_KEY = os.environ["REAL_API_KEY"]                # 从 Space Secrets 读取
FAKE_KEY = os.environ.get("FAKE_KEY", "123456")         # 你本地使用的假 key

# Space 容器可直接访问外网,无需代理
LOCAL_PROXY = None

# ==========================================

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("proxy")

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.session = aiohttp.ClientSession()
    yield
    if app.state.session:
        await app.state.session.close()

app = FastAPI(lifespan=lifespan)

@app.api_route("/v1", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
@app.api_route("/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
async def proxy_endpoint(path: str = "", request: Request = None):
    if request.method == "OPTIONS":
        return Response(status_code=200, headers={
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
            "Access-Control-Allow-Headers": "Authorization, Content-Type"
        })

    # 验证本地传来的假 key
    auth_header = request.headers.get("authorization", "")
    if auth_header.lower() != f"bearer {FAKE_KEY}":
        return JSONResponse(
            status_code=401,
            content={"error": {"message": "Invalid local API key", "type": "invalid_request_error"}}
        )

    target_url = f"{TARGET_BASE_URL.rstrip('/')}/{path}" if path else TARGET_BASE_URL.rstrip('/')

    # 构建转发 headers
    headers = {}
    for key, value in request.headers.items():
        if key.lower() in ["host", "content-length", "connection", "authorization", "transfer-encoding"]:
            continue
        headers[key] = value
    headers["Authorization"] = f"Bearer {REAL_API_KEY}"

    body = await request.body()
    query_params = dict(request.query_params)

    # 判断是否流式请求
    is_stream = False
    if body:
        try:
            is_stream = json.loads(body).get("stream", False)
        except json.JSONDecodeError:
            pass

    session = request.app.state.session
    timeout = aiohttp.ClientTimeout(total=None, connect=10, sock_read=60)

    # 转发请求到 DeepInfra
    resp = await session.request(
        method=request.method,
        url=target_url,
        headers=headers,
        data=body,
        params=query_params,
        timeout=timeout,
        proxy=LOCAL_PROXY
    )

    # 流式响应
    if is_stream or "text/event-stream" in resp.headers.get("content-type", ""):
        async def stream_generator():
            try:
                async for chunk in resp.content.iter_any():
                    yield chunk
            except (aiohttp.ClientConnectionError, aiohttp.ServerDisconnectedError,
                    aiohttp.ClientPayloadError, TimeoutError, asyncio.TimeoutError,
                    asyncio.CancelledError):
                pass
            finally:
                resp.close()

        resp_headers = {
            "content-type": resp.headers.get("content-type", "text/event-stream"),
            "cache-control": "no-cache",
            "connection": "keep-alive",
            "access-control-allow-origin": "*"
        }
        return StreamingResponse(
            stream_generator(),
            media_type=resp_headers["content-type"],
            headers=resp_headers
        )

    # 非流式响应
    else:
        content = await resp.read()
        resp.close()

        # 调试日志(可保留,帮助排查)
        logger.warning(f"UPSTREAM STATUS: {resp.status}")
        logger.warning(f"UPSTREAM BODY LENGTH: {len(content)}")
        if content:
            logger.warning(f"UPSTREAM BODY PREVIEW: {content[:200]}")

        resp_headers = {
            "content-type": resp.headers.get("content-type", "application/json"),
            "access-control-allow-origin": "*"
        }
        # 避免某些编码头干扰客户端解析
        for k in ["content-encoding", "transfer-encoding"]:
            resp_headers.pop(k, None)

        return Response(content=content, status_code=resp.status, headers=resp_headers)