Skip to content

Commit c0315b3

Browse files
authored
Merge pull request #108 from Datura-ai/hotfix-main-bittensor
Hotfix main bittensor
2 parents 51a6949 + 6b81033 commit c0315b3

25 files changed

+1633
-299
lines changed

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,14 @@ pm2 start start_validator.py --interpreter python3 -- --wallet_name "default" --
243243
```
244244
---
245245

246+
# Cursor App Setup
247+
add env varialbe CURSOR_API_KEY to .env file
248+
go to inside of cursor directory and run this command.
249+
```bash
250+
pm2 start start_cursor.sh --name cursor_app
251+
```
252+
for settings in cursor. Pls check this [Cursor README](./cursor/readme.md)
253+
246254
## License
247255
This repository is licensed under the MIT License.
248256
```text

cortext/protocol.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ class StreamPrompting(bt.StreamingSynapse):
314314
default={},
315315
title="miner_info",
316316
)
317-
time_taken: int = pydantic.Field(
317+
time_taken: float = pydantic.Field(
318318
default=0,
319319
title="time_taken",
320320
)

cursor/Dockerfile

-18
This file was deleted.

cursor/app/constants.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
llm_models = [
2+
{
3+
"id": "gpt-4o",
4+
"name": "Meta: gpt-4o Instruct",
5+
"created": 8192,
6+
"description": "OpenAI gpt-4o model.",
7+
"context_length": 8192,
8+
"architecture": {"modality": "text->text", "tokenizer": "gpt-4o", "instruct_type": "gpt-4o"},
9+
"pricing": {"prompt": "0.000000001", "completion": "0.000000001", "image": "0", "request": "0"},
10+
},
11+
{
12+
"id": "chat-claude-3-5-sonnet-20240620",
13+
"name": "Meta: claude-3-5-sonnet-20240620 Instruct",
14+
"created": 8192,
15+
"description": "claude-3-5-sonnet-20240620.",
16+
"context_length": 8192,
17+
"architecture": {"modality": "text->text", "tokenizer": "claude-3-5-sonnet-20240620", "instruct_type": "claude-3-5-sonnet-20240620"},
18+
"pricing": {"prompt": "0.000000001", "completion": "0.000000001", "image": "0", "request": "0"},
19+
},
20+
{
21+
"id": "chat-llama-3.1-70b-versatile",
22+
"name": "Meta: llama-3.1-70b-versatile Instruct",
23+
"created": 8192,
24+
"description": "llama-3.1-70b-versatile.",
25+
"context_length": 8192,
26+
"architecture": {"modality": "text->text", "tokenizer": "llama-3.1-70b-versatile", "instruct_type": "llama-3.1-70b-versatile"},
27+
"pricing": {"prompt": "0.00000001", "completion": "0.000000001", "image": "0", "request": "0"},
28+
}
29+
]

cursor/app/core/config.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import dataclasses
2+
from dataclasses import dataclass
3+
from dotenv import load_dotenv
4+
import os
5+
6+
# Load environment variables from .env file
7+
load_dotenv()
8+
9+
10+
@dataclass
11+
class Config:
12+
wallet_name: str
13+
wallet_hotkey: str
14+
api_key: str
15+
16+
@staticmethod
17+
def from_env() -> "Config":
18+
"""Load configuration from environment variables."""
19+
return Config(
20+
wallet_name=os.getenv("WALLET_NAME", "default"), # Default to an empty string if not set
21+
wallet_hotkey=os.getenv("HOT_KEY", "default"),
22+
api_key=os.getenv("CURSOR_API_KEY", "")
23+
)
24+
25+
26+
# Load config
27+
config = Config.from_env()
28+

cursor/app/core/dendrite.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import asyncio
2+
from typing import Union, AsyncGenerator, Any
3+
4+
import aiohttp
5+
import bittensor as bt
6+
from aiohttp import ServerTimeoutError, ClientConnectorError, ClientConnectionError
7+
from bittensor import dendrite
8+
import traceback
9+
import time
10+
from typing import Optional, List
11+
12+
from .protocol import StreamPrompting
13+
14+
15+
class CortexDendrite(dendrite):
16+
task_id = 0
17+
miner_to_session = {}
18+
19+
def __init__(
20+
self, wallet: Optional[Union[bt.wallet, bt.Keypair]] = None
21+
):
22+
super().__init__(wallet)
23+
24+
async def call_stream(
25+
self,
26+
target_axon: Union[bt.AxonInfo, bt.axon],
27+
synapse: bt.StreamingSynapse = bt.Synapse(), # type: ignore
28+
timeout: float = 12.0,
29+
deserialize: bool = True,
30+
organic: bool = True
31+
) -> AsyncGenerator[Any, Any]:
32+
start_time = time.time()
33+
target_axon = (
34+
target_axon.info()
35+
if isinstance(target_axon, bt.axon)
36+
else target_axon
37+
)
38+
39+
# Build request endpoint from the synapse class
40+
request_name = synapse.__class__.__name__
41+
endpoint = (
42+
f"0.0.0.0:{str(target_axon.port)}"
43+
if target_axon.ip == str(self.external_ip)
44+
else f"{target_axon.ip}:{str(target_axon.port)}"
45+
)
46+
url = f"http://{endpoint}/{request_name}"
47+
48+
# Preprocess synapse for making a request
49+
synapse: StreamPrompting = self.preprocess_synapse_for_request(target_axon, synapse, timeout) # type: ignore
50+
max_try = 0
51+
timeout = aiohttp.ClientTimeout(total=100, connect=timeout, sock_connect=timeout, sock_read=timeout)
52+
connector = aiohttp.TCPConnector(limit=200)
53+
session = aiohttp.ClientSession(timeout=timeout, connector=connector)
54+
try:
55+
while max_try < 2:
56+
async with session.post(
57+
url,
58+
headers=synapse.to_headers(),
59+
json=synapse.dict(),
60+
) as response:
61+
# Use synapse subclass' process_streaming_response method to yield the response chunks
62+
try:
63+
async for chunk in synapse.process_streaming_response(response, organic): # type: ignore
64+
yield chunk # Yield each chunk as it's processed
65+
except aiohttp.client_exceptions.ClientPayloadError:
66+
pass
67+
except ConnectionRefusedError as err:
68+
bt.logging.error(f"can not connect to miner for now. connection failed")
69+
max_try += 1
70+
continue
71+
except ClientConnectorError as err:
72+
bt.logging.error(f"can not connect to miner for now. retrying")
73+
max_try += 1
74+
continue
75+
except ClientConnectionError as err:
76+
bt.logging.error(f"can not connect to miner for now. retrying")
77+
max_try += 1
78+
continue
79+
except ServerTimeoutError as err:
80+
bt.logging.error(f"timeout error happens. max_try is {max_try}")
81+
max_try += 1
82+
continue
83+
except Exception as err:
84+
bt.logging.error(f"{err} issue from miner {synapse.uid} {synapse.provider} {synapse.model}")
85+
finally:
86+
pass
87+
break
88+
89+
except Exception as e:
90+
bt.logging.error(f"{e} {traceback.format_exc()}")
91+
finally:
92+
synapse.dendrite.process_time = str(time.time() - start_time)
93+
await session.close()
94+
95+
async def call_stream_in_batch(
96+
self,
97+
target_axons: List[Union[bt.AxonInfo, bt.axon]],
98+
synapses: List[bt.StreamingSynapse] = bt.Synapse(), # type: ignore
99+
timeout: float = 12.0,
100+
deserialize: bool = True,
101+
) -> AsyncGenerator[Any, Any]:
102+
pass

cursor/app/core/middleware.py

+24-28
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,29 @@
1-
import time
2-
from fastapi import HTTPException
1+
from fastapi import FastAPI, Depends, HTTPException, Request
2+
from cursor.app.core.config import config
3+
from starlette.middleware.base import BaseHTTPMiddleware
4+
from starlette.responses import JSONResponse
5+
# Your predefined valid API keys
6+
VALID_API_KEYS = {config.api_key}
37

48

5-
async def verify_api_key_rate_limit(config, api_key):
6-
# NOTE: abit dangerous but very useful
7-
if not config.prod:
8-
if api_key == "test":
9-
return True
9+
class APIKeyMiddleware(BaseHTTPMiddleware):
10+
async def dispatch(self, request: Request, call_next):
11+
# Get the API key from the `Authorization` header
12+
if request.method == "OPTIONS":
13+
return await call_next(request)
1014

11-
rate_limit_key = f"rate_limit:{api_key}"
12-
rate_limit = await config.redis_db.get(rate_limit_key)
13-
if rate_limit is None:
14-
async with await config.psql_db.connection() as connection:
15-
# rate_limit = await get_api_key_rate_limit(connection, api_key)
16-
if rate_limit is None:
17-
raise HTTPException(status_code=403, detail="Invalid API key")
18-
await config.redis_db.set(rate_limit_key, rate_limit, ex=30)
19-
else:
20-
rate_limit = int(rate_limit)
15+
if not request.headers.get("Authorization"):
16+
return JSONResponse(
17+
{"detail": "Invalid or missing API Key"}, status_code=401
18+
)
2119

22-
minute = time.time() // 60
23-
current_rate_limit_key = f"current_rate_limit:{api_key}:{minute}"
24-
current_rate_limit = await config.redis_db.get(current_rate_limit_key)
25-
if current_rate_limit is None:
26-
current_rate_limit = 0
27-
await config.redis_db.expire(current_rate_limit_key, 60)
28-
else:
29-
current_rate_limit = int(current_rate_limit)
20+
api_key = request.headers.get("Authorization").split(" ")[1]
3021

31-
await config.redis_db.incr(current_rate_limit_key)
32-
if current_rate_limit >= rate_limit:
33-
raise HTTPException(status_code=429, detail="Too many requests")
22+
# Validate the API key
23+
if not api_key or api_key not in VALID_API_KEYS:
24+
return JSONResponse(
25+
{"detail": "Invalid or missing API Key"}, status_code=401
26+
)
27+
28+
# Proceed to the next middleware or route handler
29+
return await call_next(request)

0 commit comments

Comments
 (0)