File size: 7,589 Bytes
ad4dcc3
ce42a46
 
 
 
 
 
 
 
 
 
 
 
15fe6a5
ce42a46
 
 
 
 
 
 
 
ad4dcc3
ce42a46
 
ad4dcc3
ce42a46
ad4dcc3
ce42a46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c4a90a3
 
 
 
 
 
 
 
 
 
 
 
 
ce42a46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3e17f5
e215d18
ce42a46
f86d874
 
ce42a46
5773582
4b750bc
ce42a46
 
 
 
e3e17f5
 
 
 
 
 
 
 
 
f86d874
 
e3e17f5
 
 
 
f86d874
ce42a46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from datetime import datetime, timedelta
from functools import partial
from os import environ
from typing import Callable, Coroutine

from anyio import create_task_group
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.responses import StreamingResponse
from httpx import AsyncClient, RequestError, Timeout
from starlette.types import Receive, Scope, Send

API_KEYS = [line for line in environ['API_KEYS'].strip().split('\n') if line and line.startswith('sk-')]
print(f'всего ключей: {len(API_KEYS)}')
COMPLETIONS_URL = 'https://openrouter.ai/api/v1/chat/completions'
app = FastAPI(title='reverse-proxy')


class Cache:
    def __init__(self, expire: timedelta):
        self.expire = expire
        self.cache = {}
        self.timestamp = datetime.now()

    async def get(self, key):
        if datetime.now() - self.timestamp > self.expire:
            self.cache.clear()
            self.timestamp = datetime.now()
        return self.cache.get(key)

    async def set(self, key, value):
        self.cache[key] = value


cache = Cache(expire=timedelta(hours=1))


def cache_results(func):
    async def wrapper(*args, **kwargs):
        cache_key = f"{func.__name__}:{args}:{kwargs}"
        cached_result = await cache.get(cache_key)
        if cached_result is not None:
            return cached_result
        result = await func(*args, **kwargs)
        await cache.set(cache_key, result)
        return result

    return wrapper


class AuthError(Exception):
    pass


class CensoredError(Exception):
    pass


@app.middleware('http')
async def add_cors_headers(request: Request, call_next):
    response = await call_next(request)
    response.headers['Access-Control-Allow-Origin'] = '*'
    response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, PATCH, OPTIONS'
    response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
    return response


@app.get('/')
async def root():
    return HTMLResponse('ну пролапс, ну и что')


class OverrideStreamResponse(StreamingResponse):
    async def stream_response(self, send: Send) -> None:
        first_chunk = True
        async for chunk in self.body_iterator:
            if first_chunk:
                await self.send_request_header(send)
                first_chunk = False
            if not isinstance(chunk, bytes):
                chunk = chunk.encode(self.charset)
            await send({'type': 'http.response.body', 'body': chunk, 'more_body': True})

        if first_chunk:
            await self.send_request_header(send)
        await send({'type': 'http.response.body', 'body': b'', 'more_body': False})

    async def send_request_header(self, send: Send) -> None:
        await send({'type': 'http.response.start', 'status': self.status_code, 'headers': self.raw_headers, })

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        async with create_task_group() as task_group:
            async def wrap(func: Callable[[], Coroutine]) -> None:
                await func()
                task_group.cancel_scope.cancel()

            task_group.start_soon(wrap, partial(self.stream_response, send))
            await wrap(partial(self.listen_for_disconnect, receive))

        if self.background is not None:
            await self.background()



async def get_response_with_valid_api_key(request: Request):
    for api_key in API_KEYS:
        try:
            response_generator = stream_api_response(api_key, request)
            response = OverrideStreamResponse(response_generator)
            return response
        except AuthError:
            print(f'ключ API {api_key} недействителен или превышен лимит отправки запросов\n всего ключей: {len(API_KEYS)}')
            continue
    raise HTTPException(status_code=401, detail='все ключи API использованы, доступ запрещен.')


async def proxy_openai_api(request: Request):
    headers = {k: v for k, v in request.headers.items() if k not in {'host', 'content-length', 'x-forwarded-for', 'x-real-ip', 'connection'}}

    def update_authorization_header(api_key):
        auth_header_key = next((k for k in headers.keys() if k.lower() == 'authorization'), 'Authorization')
        headers[auth_header_key] = f'Bearer {api_key}'

    client = AsyncClient(verify=False, follow_redirects=True, timeout=Timeout(connect=10, read=90, write=10, pool=10))

    request_body = await request.json() if request.method in {'POST', 'PUT'} else None

    async def stream_api_response(api_key: str):
        update_authorization_header(api_key)
        try:
            streaming = client.stream(request.method, COMPLETIONS_URL, headers=headers, params=request.query_params, json=request_body)
            async with streaming as stream_response:
                if stream_response.status_code in {401, 402, 429}:
                    yield 'auth_error'
                    return
                if stream_response.status_code == 403:
                    raise CensoredError('отклонено по цензуре')
                
                async for chunk in stream_response.aiter_bytes():
                    if chunk.strip():
                        yield chunk.strip()

        except RequestError as exc:
            raise HTTPException(status_code=500, detail=f'произошла ошибка при запросе: {exc}')

    async def get_response():
        for api_key in API_KEYS:
            response_generator = stream_api_response(api_key)
            try:
                first_chunk = await response_generator.__anext__()
                if first_chunk == 'auth_error':
                    print(f'ключ API {api_key} недействителен или превышен лимит отправки запросов')
                    continue
                else:
                    headers_to_forward = {k: v for k, v in headers.items() if k.lower() not in {'content-length', 'content-encoding', 'alt-svc'}}
                    return OverrideStreamResponse(itertools.chain([first_chunk], response_generator), headers=headers_to_forward)
            except StopAsyncIteration:
                continue
        raise HTTPException(status_code=401, detail='все ключи API использованы, доступ запрещен.')
    
    return await get_response()


@cache_results
async def get_free_models():
    async with AsyncClient(follow_redirects=True, timeout=Timeout(10.0, read=30.0, write=10.0, pool=10.0)) as client:
        response = await client.get('https://openrouter.ai/api/v1/models')
        response.raise_for_status()
        data = response.json()
    filtered_models = [model for model in data.get('data', []) if model.get('id', '').endswith(':free')]
    return {'data': filtered_models, 'object': 'list'}


@app.api_route('/v1/models', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
@app.api_route('/models', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
async def get_models():
    return await get_free_models()


@app.api_route('/v1/chat/completions', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
@app.api_route('/chat/completions', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
async def proxy_handler(request: Request):
    return await proxy_openai_api(request)


if __name__ == '__main__':
    from uvicorn import run

    run(app, host='0.0.0.0', port=7860)