webscout 8.3.4__py3-none-any.whl → 8.3.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of webscout might be problematic. Click here for more details.
- webscout/AIutel.py +52 -1016
- webscout/Provider/AISEARCH/__init__.py +11 -10
- webscout/Provider/AISEARCH/felo_search.py +7 -3
- webscout/Provider/AISEARCH/scira_search.py +2 -0
- webscout/Provider/AISEARCH/stellar_search.py +53 -8
- webscout/Provider/Deepinfra.py +7 -1
- webscout/Provider/OPENAI/TogetherAI.py +57 -48
- webscout/Provider/OPENAI/TwoAI.py +94 -1
- webscout/Provider/OPENAI/__init__.py +0 -2
- webscout/Provider/OPENAI/deepinfra.py +6 -0
- webscout/Provider/OPENAI/scirachat.py +4 -0
- webscout/Provider/OPENAI/textpollinations.py +11 -7
- webscout/Provider/OPENAI/venice.py +1 -0
- webscout/Provider/Perplexitylabs.py +163 -147
- webscout/Provider/Qodo.py +30 -6
- webscout/Provider/TTI/__init__.py +1 -0
- webscout/Provider/TTI/together.py +7 -6
- webscout/Provider/TTI/venice.py +368 -0
- webscout/Provider/TextPollinationsAI.py +11 -7
- webscout/Provider/TogetherAI.py +57 -44
- webscout/Provider/TwoAI.py +96 -2
- webscout/Provider/TypliAI.py +33 -27
- webscout/Provider/UNFINISHED/PERPLEXED_search.py +254 -0
- webscout/Provider/UNFINISHED/fetch_together_models.py +6 -11
- webscout/Provider/Venice.py +1 -0
- webscout/Provider/WiseCat.py +18 -20
- webscout/Provider/__init__.py +0 -6
- webscout/Provider/scira_chat.py +4 -0
- webscout/Provider/toolbaz.py +5 -10
- webscout/Provider/typefully.py +1 -11
- webscout/__init__.py +3 -15
- webscout/auth/__init__.py +19 -4
- webscout/auth/api_key_manager.py +189 -189
- webscout/auth/auth_system.py +25 -40
- webscout/auth/config.py +105 -6
- webscout/auth/database.py +377 -22
- webscout/auth/models.py +185 -130
- webscout/auth/request_processing.py +175 -11
- webscout/auth/routes.py +99 -2
- webscout/auth/server.py +9 -2
- webscout/auth/simple_logger.py +236 -0
- webscout/sanitize.py +1074 -0
- webscout/version.py +1 -1
- {webscout-8.3.4.dist-info → webscout-8.3.5.dist-info}/METADATA +9 -149
- {webscout-8.3.4.dist-info → webscout-8.3.5.dist-info}/RECORD +49 -51
- webscout/Provider/OPENAI/README_AUTOPROXY.md +0 -238
- webscout/Provider/OPENAI/typegpt.py +0 -368
- webscout/Provider/OPENAI/uncovrAI.py +0 -477
- webscout/Provider/WritingMate.py +0 -273
- webscout/Provider/typegpt.py +0 -284
- webscout/Provider/uncovr.py +0 -333
- {webscout-8.3.4.dist-info → webscout-8.3.5.dist-info}/WHEEL +0 -0
- {webscout-8.3.4.dist-info → webscout-8.3.5.dist-info}/entry_points.txt +0 -0
- {webscout-8.3.4.dist-info → webscout-8.3.5.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-8.3.4.dist-info → webscout-8.3.5.dist-info}/top_level.txt +0 -0
webscout/auth/models.py
CHANGED
|
@@ -1,130 +1,185 @@
|
|
|
1
|
-
# webscout/auth/models.py
|
|
2
|
-
|
|
3
|
-
from datetime import datetime, timezone
|
|
4
|
-
from typing import Optional, Dict, Any, List
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
import uuid
|
|
7
|
-
import json
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@dataclass
|
|
11
|
-
class User:
|
|
12
|
-
"""User model for authentication system."""
|
|
13
|
-
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
14
|
-
username: str = ""
|
|
15
|
-
telegram_id: int = field(default_factory=lambda: 0) # Required Telegram ID as number only
|
|
16
|
-
|
|
17
|
-
def validate_telegram_id(self) -> None:
|
|
18
|
-
"""Ensure telegram_id is an integer."""
|
|
19
|
-
if not isinstance(self.telegram_id, int):
|
|
20
|
-
raise ValueError("telegram_id must be an integer.")
|
|
21
|
-
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
22
|
-
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
23
|
-
is_active: bool = True
|
|
24
|
-
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
25
|
-
|
|
26
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
27
|
-
"""Convert user to dictionary for storage."""
|
|
28
|
-
return {
|
|
29
|
-
"id": self.id,
|
|
30
|
-
"username": self.username,
|
|
31
|
-
"telegram_id": self.telegram_id,
|
|
32
|
-
"created_at": self.created_at.isoformat(),
|
|
33
|
-
"updated_at": self.updated_at.isoformat(),
|
|
34
|
-
"is_active": self.is_active,
|
|
35
|
-
"metadata": self.metadata
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
@classmethod
|
|
39
|
-
def from_dict(cls, data: Dict[str, Any]) -> "User":
|
|
40
|
-
"""Create user from dictionary."""
|
|
41
|
-
return cls(
|
|
42
|
-
id=data.get("id", str(uuid.uuid4())),
|
|
43
|
-
username=data.get("username", ""),
|
|
44
|
-
telegram_id=int(data.get("telegram_id", 0)),
|
|
45
|
-
created_at=datetime.fromisoformat(data.get("created_at", datetime.now(timezone.utc).isoformat())),
|
|
46
|
-
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.now(timezone.utc).isoformat())),
|
|
47
|
-
is_active=data.get("is_active", True),
|
|
48
|
-
metadata=data.get("metadata", {})
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@dataclass
|
|
53
|
-
class APIKey:
|
|
54
|
-
"""API Key model for authentication system."""
|
|
55
|
-
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
56
|
-
key: str = ""
|
|
57
|
-
user_id: str = ""
|
|
58
|
-
name: Optional[str] = None
|
|
59
|
-
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
60
|
-
last_used_at: Optional[datetime] = None
|
|
61
|
-
expires_at: Optional[datetime] = None
|
|
62
|
-
is_active: bool = True
|
|
63
|
-
rate_limit: int = 10 # requests per minute
|
|
64
|
-
usage_count: int = 0
|
|
65
|
-
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
66
|
-
|
|
67
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
68
|
-
"""Convert API key to dictionary for storage."""
|
|
69
|
-
return {
|
|
70
|
-
"id": self.id,
|
|
71
|
-
"key": self.key,
|
|
72
|
-
"user_id": self.user_id,
|
|
73
|
-
"name": self.name,
|
|
74
|
-
"created_at": self.created_at.isoformat(),
|
|
75
|
-
"last_used_at": self.last_used_at.isoformat() if self.last_used_at else None,
|
|
76
|
-
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
|
77
|
-
"is_active": self.is_active,
|
|
78
|
-
"rate_limit": self.rate_limit,
|
|
79
|
-
"usage_count": self.usage_count,
|
|
80
|
-
"metadata": self.metadata
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@classmethod
|
|
84
|
-
def from_dict(cls, data: Dict[str, Any]) -> "APIKey":
|
|
85
|
-
"""Create API key from dictionary."""
|
|
86
|
-
return cls(
|
|
87
|
-
id=data.get("id", str(uuid.uuid4())),
|
|
88
|
-
key=data.get("key", ""),
|
|
89
|
-
user_id=data.get("user_id", ""),
|
|
90
|
-
name=data.get("name"),
|
|
91
|
-
created_at=datetime.fromisoformat(data.get("created_at", datetime.now(timezone.utc).isoformat())),
|
|
92
|
-
last_used_at=datetime.fromisoformat(data["last_used_at"]) if data.get("last_used_at") else None,
|
|
93
|
-
expires_at=datetime.fromisoformat(data["expires_at"]) if data.get("expires_at") else None,
|
|
94
|
-
is_active=data.get("is_active", True),
|
|
95
|
-
rate_limit=data.get("rate_limit", 10),
|
|
96
|
-
usage_count=data.get("usage_count", 0),
|
|
97
|
-
metadata=data.get("metadata", {})
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
def is_expired(self) -> bool:
|
|
101
|
-
"""Check if API key is expired."""
|
|
102
|
-
if not self.expires_at:
|
|
103
|
-
return False
|
|
104
|
-
return datetime.now(timezone.utc) > self.expires_at
|
|
105
|
-
|
|
106
|
-
def is_valid(self) -> bool:
|
|
107
|
-
"""Check if API key is valid (active and not expired)."""
|
|
108
|
-
return self.is_active and not self.is_expired()
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@dataclass
|
|
112
|
-
class RateLimitEntry:
|
|
113
|
-
"""Rate limit tracking entry."""
|
|
114
|
-
api_key_id: str
|
|
115
|
-
requests: List[datetime] = field(default_factory=list)
|
|
116
|
-
|
|
117
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
118
|
-
"""Convert to dictionary for storage."""
|
|
119
|
-
return {
|
|
120
|
-
"api_key_id": self.api_key_id,
|
|
121
|
-
"requests": [req.isoformat() for req in self.requests]
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
@classmethod
|
|
125
|
-
def from_dict(cls, data: Dict[str, Any]) -> "RateLimitEntry":
|
|
126
|
-
"""Create from dictionary."""
|
|
127
|
-
return cls(
|
|
128
|
-
api_key_id=data.get("api_key_id", ""),
|
|
129
|
-
requests=[datetime.fromisoformat(req) for req in data.get("requests", [])]
|
|
130
|
-
)
|
|
1
|
+
# webscout/auth/models.py
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Optional, Dict, Any, List
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
import uuid
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class User:
|
|
12
|
+
"""User model for authentication system."""
|
|
13
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
14
|
+
username: str = ""
|
|
15
|
+
telegram_id: int = field(default_factory=lambda: 0) # Required Telegram ID as number only
|
|
16
|
+
|
|
17
|
+
def validate_telegram_id(self) -> None:
|
|
18
|
+
"""Ensure telegram_id is an integer."""
|
|
19
|
+
if not isinstance(self.telegram_id, int):
|
|
20
|
+
raise ValueError("telegram_id must be an integer.")
|
|
21
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
22
|
+
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
23
|
+
is_active: bool = True
|
|
24
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
27
|
+
"""Convert user to dictionary for storage."""
|
|
28
|
+
return {
|
|
29
|
+
"id": self.id,
|
|
30
|
+
"username": self.username,
|
|
31
|
+
"telegram_id": self.telegram_id,
|
|
32
|
+
"created_at": self.created_at.isoformat(),
|
|
33
|
+
"updated_at": self.updated_at.isoformat(),
|
|
34
|
+
"is_active": self.is_active,
|
|
35
|
+
"metadata": self.metadata
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_dict(cls, data: Dict[str, Any]) -> "User":
|
|
40
|
+
"""Create user from dictionary."""
|
|
41
|
+
return cls(
|
|
42
|
+
id=data.get("id", str(uuid.uuid4())),
|
|
43
|
+
username=data.get("username", ""),
|
|
44
|
+
telegram_id=int(data.get("telegram_id", 0)),
|
|
45
|
+
created_at=datetime.fromisoformat(data.get("created_at", datetime.now(timezone.utc).isoformat())),
|
|
46
|
+
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.now(timezone.utc).isoformat())),
|
|
47
|
+
is_active=data.get("is_active", True),
|
|
48
|
+
metadata=data.get("metadata", {})
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class APIKey:
|
|
54
|
+
"""API Key model for authentication system."""
|
|
55
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
56
|
+
key: str = ""
|
|
57
|
+
user_id: str = ""
|
|
58
|
+
name: Optional[str] = None
|
|
59
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
60
|
+
last_used_at: Optional[datetime] = None
|
|
61
|
+
expires_at: Optional[datetime] = None
|
|
62
|
+
is_active: bool = True
|
|
63
|
+
rate_limit: int = 10 # requests per minute
|
|
64
|
+
usage_count: int = 0
|
|
65
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
68
|
+
"""Convert API key to dictionary for storage."""
|
|
69
|
+
return {
|
|
70
|
+
"id": self.id,
|
|
71
|
+
"key": self.key,
|
|
72
|
+
"user_id": self.user_id,
|
|
73
|
+
"name": self.name,
|
|
74
|
+
"created_at": self.created_at.isoformat(),
|
|
75
|
+
"last_used_at": self.last_used_at.isoformat() if self.last_used_at else None,
|
|
76
|
+
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
|
77
|
+
"is_active": self.is_active,
|
|
78
|
+
"rate_limit": self.rate_limit,
|
|
79
|
+
"usage_count": self.usage_count,
|
|
80
|
+
"metadata": self.metadata
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, data: Dict[str, Any]) -> "APIKey":
|
|
85
|
+
"""Create API key from dictionary."""
|
|
86
|
+
return cls(
|
|
87
|
+
id=data.get("id", str(uuid.uuid4())),
|
|
88
|
+
key=data.get("key", ""),
|
|
89
|
+
user_id=data.get("user_id", ""),
|
|
90
|
+
name=data.get("name"),
|
|
91
|
+
created_at=datetime.fromisoformat(data.get("created_at", datetime.now(timezone.utc).isoformat())),
|
|
92
|
+
last_used_at=datetime.fromisoformat(data["last_used_at"]) if data.get("last_used_at") else None,
|
|
93
|
+
expires_at=datetime.fromisoformat(data["expires_at"]) if data.get("expires_at") else None,
|
|
94
|
+
is_active=data.get("is_active", True),
|
|
95
|
+
rate_limit=data.get("rate_limit", 10),
|
|
96
|
+
usage_count=data.get("usage_count", 0),
|
|
97
|
+
metadata=data.get("metadata", {})
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def is_expired(self) -> bool:
|
|
101
|
+
"""Check if API key is expired."""
|
|
102
|
+
if not self.expires_at:
|
|
103
|
+
return False
|
|
104
|
+
return datetime.now(timezone.utc) > self.expires_at
|
|
105
|
+
|
|
106
|
+
def is_valid(self) -> bool:
|
|
107
|
+
"""Check if API key is valid (active and not expired)."""
|
|
108
|
+
return self.is_active and not self.is_expired()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class RateLimitEntry:
|
|
113
|
+
"""Rate limit tracking entry."""
|
|
114
|
+
api_key_id: str
|
|
115
|
+
requests: List[datetime] = field(default_factory=list)
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
118
|
+
"""Convert to dictionary for storage."""
|
|
119
|
+
return {
|
|
120
|
+
"api_key_id": self.api_key_id,
|
|
121
|
+
"requests": [req.isoformat() for req in self.requests]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def from_dict(cls, data: Dict[str, Any]) -> "RateLimitEntry":
|
|
126
|
+
"""Create from dictionary."""
|
|
127
|
+
return cls(
|
|
128
|
+
api_key_id=data.get("api_key_id", ""),
|
|
129
|
+
requests=[datetime.fromisoformat(req) for req in data.get("requests", [])]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class RequestLog:
|
|
135
|
+
"""Request log entry for API usage tracking."""
|
|
136
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
137
|
+
request_id: str = ""
|
|
138
|
+
ip_address: str = ""
|
|
139
|
+
model_used: str = ""
|
|
140
|
+
question: str = ""
|
|
141
|
+
answer: str = ""
|
|
142
|
+
user_id: Optional[str] = None
|
|
143
|
+
api_key_id: Optional[str] = None
|
|
144
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
145
|
+
response_time_ms: Optional[int] = None
|
|
146
|
+
status_code: int = 200
|
|
147
|
+
error_message: Optional[str] = None
|
|
148
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
149
|
+
|
|
150
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
151
|
+
"""Convert request log to dictionary for storage."""
|
|
152
|
+
return {
|
|
153
|
+
"id": self.id,
|
|
154
|
+
"request_id": self.request_id,
|
|
155
|
+
"ip_address": self.ip_address,
|
|
156
|
+
"model_used": self.model_used,
|
|
157
|
+
"question": self.question,
|
|
158
|
+
"answer": self.answer,
|
|
159
|
+
"user_id": self.user_id,
|
|
160
|
+
"api_key_id": self.api_key_id,
|
|
161
|
+
"created_at": self.created_at.isoformat(),
|
|
162
|
+
"response_time_ms": self.response_time_ms,
|
|
163
|
+
"status_code": self.status_code,
|
|
164
|
+
"error_message": self.error_message,
|
|
165
|
+
"metadata": self.metadata
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def from_dict(cls, data: Dict[str, Any]) -> "RequestLog":
|
|
170
|
+
"""Create request log from dictionary."""
|
|
171
|
+
return cls(
|
|
172
|
+
id=data.get("id", str(uuid.uuid4())),
|
|
173
|
+
request_id=data.get("request_id", ""),
|
|
174
|
+
ip_address=data.get("ip_address", ""),
|
|
175
|
+
model_used=data.get("model_used", ""),
|
|
176
|
+
question=data.get("question", ""),
|
|
177
|
+
answer=data.get("answer", ""),
|
|
178
|
+
user_id=data.get("user_id"),
|
|
179
|
+
api_key_id=data.get("api_key_id"),
|
|
180
|
+
created_at=datetime.fromisoformat(data.get("created_at", datetime.now(timezone.utc).isoformat())),
|
|
181
|
+
response_time_ms=data.get("response_time_ms"),
|
|
182
|
+
status_code=data.get("status_code", 200),
|
|
183
|
+
error_message=data.get("error_message"),
|
|
184
|
+
metadata=data.get("metadata", {})
|
|
185
|
+
)
|
|
@@ -6,6 +6,7 @@ import json
|
|
|
6
6
|
import time
|
|
7
7
|
import uuid
|
|
8
8
|
from typing import List, Dict, Any
|
|
9
|
+
from datetime import datetime, timezone
|
|
9
10
|
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, HTTP_500_INTERNAL_SERVER_ERROR
|
|
10
11
|
from fastapi.responses import StreamingResponse
|
|
11
12
|
|
|
@@ -15,6 +16,10 @@ import sys
|
|
|
15
16
|
|
|
16
17
|
from .request_models import Message, ChatCompletionRequest
|
|
17
18
|
from .exceptions import APIError, clean_text
|
|
19
|
+
from .models import RequestLog
|
|
20
|
+
from .auth_system import get_auth_components
|
|
21
|
+
from .simple_logger import log_api_request, get_client_ip, generate_request_id
|
|
22
|
+
from .config import AppConfig
|
|
18
23
|
|
|
19
24
|
# Setup logger
|
|
20
25
|
logger = Logger(
|
|
@@ -25,6 +30,57 @@ logger = Logger(
|
|
|
25
30
|
)
|
|
26
31
|
|
|
27
32
|
|
|
33
|
+
async def log_request(request_id: str, ip_address: str, model_used: str, question: str,
|
|
34
|
+
answer: str, response_time_ms: int, status_code: int = 200,
|
|
35
|
+
error_message: str = None, provider: str = None, request_obj=None):
|
|
36
|
+
"""Log API request to database."""
|
|
37
|
+
try:
|
|
38
|
+
# Use simple logger for no-auth mode if request logging is enabled
|
|
39
|
+
if AppConfig.request_logging_enabled:
|
|
40
|
+
user_agent = None
|
|
41
|
+
if request_obj:
|
|
42
|
+
user_agent = request_obj.headers.get("user-agent")
|
|
43
|
+
|
|
44
|
+
await log_api_request(
|
|
45
|
+
request_id=request_id,
|
|
46
|
+
ip_address=ip_address,
|
|
47
|
+
model=model_used,
|
|
48
|
+
question=question,
|
|
49
|
+
answer=answer,
|
|
50
|
+
provider=provider,
|
|
51
|
+
processing_time_ms=response_time_ms,
|
|
52
|
+
error=error_message,
|
|
53
|
+
user_agent=user_agent
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Also use the existing auth system logging if available
|
|
57
|
+
auth_manager, db_manager, _ = get_auth_components()
|
|
58
|
+
|
|
59
|
+
if db_manager:
|
|
60
|
+
request_log = RequestLog(
|
|
61
|
+
id=None, # Will be auto-generated
|
|
62
|
+
request_id=request_id,
|
|
63
|
+
ip_address=ip_address,
|
|
64
|
+
model_used=model_used,
|
|
65
|
+
question=question,
|
|
66
|
+
answer=answer,
|
|
67
|
+
user_id=None, # No auth mode
|
|
68
|
+
api_key_id=None, # No auth mode
|
|
69
|
+
created_at=datetime.now(timezone.utc),
|
|
70
|
+
response_time_ms=response_time_ms,
|
|
71
|
+
status_code=status_code,
|
|
72
|
+
error_message=error_message,
|
|
73
|
+
metadata={}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
await db_manager.create_request_log(request_log)
|
|
77
|
+
logger.debug(f"Logged request {request_id} to auth database")
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Failed to log request {request_id}: {e}")
|
|
81
|
+
# Don't raise exception to avoid breaking the main request flow
|
|
82
|
+
|
|
83
|
+
|
|
28
84
|
def process_messages(messages: List[Message]) -> List[Dict[str, Any]]:
|
|
29
85
|
"""Process and validate chat messages."""
|
|
30
86
|
processed_messages = []
|
|
@@ -81,9 +137,14 @@ def prepare_provider_params(chat_request: ChatCompletionRequest, model_name: str
|
|
|
81
137
|
return params
|
|
82
138
|
|
|
83
139
|
|
|
84
|
-
async def handle_streaming_response(provider: Any, params: Dict[str, Any], request_id: str
|
|
140
|
+
async def handle_streaming_response(provider: Any, params: Dict[str, Any], request_id: str,
|
|
141
|
+
ip_address: str, question: str, model_name: str, start_time: float,
|
|
142
|
+
provider_name: str = None, request_obj=None) -> StreamingResponse:
|
|
85
143
|
"""Handle streaming chat completion response."""
|
|
144
|
+
collected_content = []
|
|
145
|
+
|
|
86
146
|
async def streaming():
|
|
147
|
+
nonlocal collected_content
|
|
87
148
|
try:
|
|
88
149
|
logger.debug(f"Starting streaming response for request {request_id}")
|
|
89
150
|
completion_stream = provider.chat.completions.create(**params)
|
|
@@ -108,10 +169,16 @@ async def handle_streaming_response(provider: Any, params: Dict[str, Any], reque
|
|
|
108
169
|
if isinstance(choice, dict):
|
|
109
170
|
# Handle delta for streaming
|
|
110
171
|
if 'delta' in choice and isinstance(choice['delta'], dict) and 'content' in choice['delta']:
|
|
111
|
-
|
|
172
|
+
content = choice['delta']['content']
|
|
173
|
+
if content:
|
|
174
|
+
collected_content.append(content)
|
|
175
|
+
choice['delta']['content'] = clean_text(content)
|
|
112
176
|
# Handle message for non-streaming
|
|
113
177
|
elif 'message' in choice and isinstance(choice['message'], dict) and 'content' in choice['message']:
|
|
114
|
-
|
|
178
|
+
content = choice['message']['content']
|
|
179
|
+
if content:
|
|
180
|
+
collected_content.append(content)
|
|
181
|
+
choice['message']['content'] = clean_text(content)
|
|
115
182
|
|
|
116
183
|
yield f"data: {json.dumps(chunk_data, ensure_ascii=False)}\n\n"
|
|
117
184
|
except TypeError as te:
|
|
@@ -129,9 +196,15 @@ async def handle_streaming_response(provider: Any, params: Dict[str, Any], reque
|
|
|
129
196
|
for choice in response_data.get('choices', []):
|
|
130
197
|
if isinstance(choice, dict):
|
|
131
198
|
if 'delta' in choice and isinstance(choice['delta'], dict) and 'content' in choice['delta']:
|
|
132
|
-
|
|
199
|
+
content = choice['delta']['content']
|
|
200
|
+
if content:
|
|
201
|
+
collected_content.append(content)
|
|
202
|
+
choice['delta']['content'] = clean_text(content)
|
|
133
203
|
elif 'message' in choice and isinstance(choice['message'], dict) and 'content' in choice['message']:
|
|
134
|
-
|
|
204
|
+
content = choice['message']['content']
|
|
205
|
+
if content:
|
|
206
|
+
collected_content.append(content)
|
|
207
|
+
choice['message']['content'] = clean_text(content)
|
|
135
208
|
|
|
136
209
|
yield f"data: {json.dumps(response_data, ensure_ascii=False)}\n\n"
|
|
137
210
|
else: # Non-generator response
|
|
@@ -147,9 +220,15 @@ async def handle_streaming_response(provider: Any, params: Dict[str, Any], reque
|
|
|
147
220
|
for choice in response_data.get('choices', []):
|
|
148
221
|
if isinstance(choice, dict):
|
|
149
222
|
if 'delta' in choice and isinstance(choice['delta'], dict) and 'content' in choice['delta']:
|
|
150
|
-
|
|
223
|
+
content = choice['delta']['content']
|
|
224
|
+
if content:
|
|
225
|
+
collected_content.append(content)
|
|
226
|
+
choice['delta']['content'] = clean_text(content)
|
|
151
227
|
elif 'message' in choice and isinstance(choice['message'], dict) and 'content' in choice['message']:
|
|
152
|
-
|
|
228
|
+
content = choice['message']['content']
|
|
229
|
+
if content:
|
|
230
|
+
collected_content.append(content)
|
|
231
|
+
choice['message']['content'] = clean_text(content)
|
|
153
232
|
|
|
154
233
|
yield f"data: {json.dumps(response_data, ensure_ascii=False)}\n\n"
|
|
155
234
|
|
|
@@ -164,13 +243,47 @@ async def handle_streaming_response(provider: Any, params: Dict[str, Any], reque
|
|
|
164
243
|
}
|
|
165
244
|
}
|
|
166
245
|
yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
|
|
246
|
+
|
|
247
|
+
# Log error request
|
|
248
|
+
response_time_ms = int((time.time() - start_time) * 1000)
|
|
249
|
+
await log_request(
|
|
250
|
+
request_id=request_id,
|
|
251
|
+
ip_address=ip_address,
|
|
252
|
+
model_used=model_name,
|
|
253
|
+
question=question,
|
|
254
|
+
answer="",
|
|
255
|
+
response_time_ms=response_time_ms,
|
|
256
|
+
status_code=500,
|
|
257
|
+
error_message=error_message,
|
|
258
|
+
provider=provider_name,
|
|
259
|
+
request_obj=request_obj
|
|
260
|
+
)
|
|
167
261
|
finally:
|
|
168
262
|
yield "data: [DONE]\n\n"
|
|
263
|
+
|
|
264
|
+
# Log successful streaming request
|
|
265
|
+
if collected_content:
|
|
266
|
+
answer = "".join(collected_content)
|
|
267
|
+
response_time_ms = int((time.time() - start_time) * 1000)
|
|
268
|
+
await log_request(
|
|
269
|
+
request_id=request_id,
|
|
270
|
+
ip_address=ip_address,
|
|
271
|
+
model_used=model_name,
|
|
272
|
+
question=question,
|
|
273
|
+
answer=answer,
|
|
274
|
+
response_time_ms=response_time_ms,
|
|
275
|
+
status_code=200,
|
|
276
|
+
provider=provider_name,
|
|
277
|
+
request_obj=request_obj
|
|
278
|
+
)
|
|
279
|
+
|
|
169
280
|
return StreamingResponse(streaming(), media_type="text/event-stream")
|
|
170
281
|
|
|
171
282
|
|
|
172
283
|
async def handle_non_streaming_response(provider: Any, params: Dict[str, Any],
|
|
173
|
-
request_id: str, start_time: float
|
|
284
|
+
request_id: str, start_time: float, ip_address: str,
|
|
285
|
+
question: str, model_name: str, provider_name: str = None,
|
|
286
|
+
request_obj=None) -> Dict[str, Any]:
|
|
174
287
|
"""Handle non-streaming chat completion response."""
|
|
175
288
|
try:
|
|
176
289
|
logger.debug(f"Starting non-streaming response for request {request_id}")
|
|
@@ -178,7 +291,7 @@ async def handle_non_streaming_response(provider: Any, params: Dict[str, Any],
|
|
|
178
291
|
|
|
179
292
|
if completion is None:
|
|
180
293
|
# Return a valid OpenAI-compatible error response
|
|
181
|
-
|
|
294
|
+
error_response = ChatCompletion(
|
|
182
295
|
id=request_id,
|
|
183
296
|
created=int(time.time()),
|
|
184
297
|
model=params.get("model", "unknown"),
|
|
@@ -189,6 +302,23 @@ async def handle_non_streaming_response(provider: Any, params: Dict[str, Any],
|
|
|
189
302
|
)],
|
|
190
303
|
usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0)
|
|
191
304
|
).model_dump(exclude_none=True)
|
|
305
|
+
|
|
306
|
+
# Log error request
|
|
307
|
+
response_time_ms = int((time.time() - start_time) * 1000)
|
|
308
|
+
await log_request(
|
|
309
|
+
request_id=request_id,
|
|
310
|
+
ip_address=ip_address,
|
|
311
|
+
model_used=model_name,
|
|
312
|
+
question=question,
|
|
313
|
+
answer="No response generated.",
|
|
314
|
+
response_time_ms=response_time_ms,
|
|
315
|
+
status_code=500,
|
|
316
|
+
error_message="No response generated from provider",
|
|
317
|
+
provider=provider_name,
|
|
318
|
+
request_obj=request_obj
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
return error_response
|
|
192
322
|
|
|
193
323
|
# Standardize response format
|
|
194
324
|
if hasattr(completion, "model_dump"): # Pydantic v2
|
|
@@ -204,21 +334,55 @@ async def handle_non_streaming_response(provider: Any, params: Dict[str, Any],
|
|
|
204
334
|
"provider_error"
|
|
205
335
|
)
|
|
206
336
|
|
|
207
|
-
#
|
|
337
|
+
# Extract answer from response and clean text content
|
|
338
|
+
answer = ""
|
|
208
339
|
if isinstance(response_data, dict) and 'choices' in response_data:
|
|
209
340
|
for choice in response_data.get('choices', []):
|
|
210
341
|
if isinstance(choice, dict) and 'message' in choice:
|
|
211
342
|
if isinstance(choice['message'], dict) and 'content' in choice['message']:
|
|
212
|
-
|
|
343
|
+
content = choice['message']['content']
|
|
344
|
+
if content:
|
|
345
|
+
answer = content
|
|
346
|
+
choice['message']['content'] = clean_text(content)
|
|
213
347
|
|
|
214
348
|
elapsed = time.time() - start_time
|
|
349
|
+
response_time_ms = int(elapsed * 1000)
|
|
215
350
|
logger.info(f"Completed non-streaming request {request_id} in {elapsed:.2f}s")
|
|
216
351
|
|
|
352
|
+
# Log successful request
|
|
353
|
+
await log_request(
|
|
354
|
+
request_id=request_id,
|
|
355
|
+
ip_address=ip_address,
|
|
356
|
+
model_used=model_name,
|
|
357
|
+
question=question,
|
|
358
|
+
answer=answer,
|
|
359
|
+
response_time_ms=response_time_ms,
|
|
360
|
+
status_code=200,
|
|
361
|
+
provider=provider_name,
|
|
362
|
+
request_obj=request_obj
|
|
363
|
+
)
|
|
364
|
+
|
|
217
365
|
return response_data
|
|
218
366
|
|
|
219
367
|
except Exception as e:
|
|
220
368
|
logger.error(f"Error in non-streaming response for request {request_id}: {e}")
|
|
221
369
|
error_message = clean_text(str(e))
|
|
370
|
+
|
|
371
|
+
# Log error request
|
|
372
|
+
response_time_ms = int((time.time() - start_time) * 1000)
|
|
373
|
+
await log_request(
|
|
374
|
+
request_id=request_id,
|
|
375
|
+
ip_address=ip_address,
|
|
376
|
+
model_used=model_name,
|
|
377
|
+
question=question,
|
|
378
|
+
answer="",
|
|
379
|
+
response_time_ms=response_time_ms,
|
|
380
|
+
status_code=500,
|
|
381
|
+
error_message=error_message,
|
|
382
|
+
provider=provider_name,
|
|
383
|
+
request_obj=request_obj
|
|
384
|
+
)
|
|
385
|
+
|
|
222
386
|
raise APIError(
|
|
223
387
|
f"Provider error: {error_message}",
|
|
224
388
|
HTTP_500_INTERNAL_SERVER_ERROR,
|