webscout 8.3.4__py3-none-any.whl → 8.3.6__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.

Files changed (98) hide show
  1. webscout/AIutel.py +52 -1016
  2. webscout/Bard.py +12 -6
  3. webscout/DWEBS.py +66 -57
  4. webscout/Provider/AISEARCH/PERPLEXED_search.py +214 -0
  5. webscout/Provider/AISEARCH/__init__.py +11 -10
  6. webscout/Provider/AISEARCH/felo_search.py +7 -3
  7. webscout/Provider/AISEARCH/scira_search.py +2 -0
  8. webscout/Provider/AISEARCH/stellar_search.py +53 -8
  9. webscout/Provider/Deepinfra.py +13 -1
  10. webscout/Provider/Flowith.py +6 -1
  11. webscout/Provider/GithubChat.py +1 -0
  12. webscout/Provider/GptOss.py +207 -0
  13. webscout/Provider/Kimi.py +445 -0
  14. webscout/Provider/Netwrck.py +3 -6
  15. webscout/Provider/OPENAI/README.md +2 -1
  16. webscout/Provider/OPENAI/TogetherAI.py +12 -8
  17. webscout/Provider/OPENAI/TwoAI.py +94 -1
  18. webscout/Provider/OPENAI/__init__.py +4 -4
  19. webscout/Provider/OPENAI/copilot.py +20 -4
  20. webscout/Provider/OPENAI/deepinfra.py +12 -0
  21. webscout/Provider/OPENAI/e2b.py +60 -8
  22. webscout/Provider/OPENAI/flowith.py +4 -3
  23. webscout/Provider/OPENAI/generate_api_key.py +48 -0
  24. webscout/Provider/OPENAI/gptoss.py +288 -0
  25. webscout/Provider/OPENAI/kimi.py +469 -0
  26. webscout/Provider/OPENAI/netwrck.py +8 -12
  27. webscout/Provider/OPENAI/refact.py +274 -0
  28. webscout/Provider/OPENAI/scirachat.py +4 -0
  29. webscout/Provider/OPENAI/textpollinations.py +11 -10
  30. webscout/Provider/OPENAI/toolbaz.py +1 -0
  31. webscout/Provider/OPENAI/venice.py +1 -0
  32. webscout/Provider/Perplexitylabs.py +163 -147
  33. webscout/Provider/Qodo.py +30 -6
  34. webscout/Provider/TTI/__init__.py +1 -0
  35. webscout/Provider/TTI/bing.py +14 -2
  36. webscout/Provider/TTI/together.py +11 -9
  37. webscout/Provider/TTI/venice.py +368 -0
  38. webscout/Provider/TTS/README.md +0 -1
  39. webscout/Provider/TTS/__init__.py +0 -1
  40. webscout/Provider/TTS/base.py +479 -159
  41. webscout/Provider/TTS/deepgram.py +409 -156
  42. webscout/Provider/TTS/elevenlabs.py +425 -111
  43. webscout/Provider/TTS/freetts.py +317 -140
  44. webscout/Provider/TTS/gesserit.py +192 -128
  45. webscout/Provider/TTS/murfai.py +248 -113
  46. webscout/Provider/TTS/openai_fm.py +347 -129
  47. webscout/Provider/TTS/speechma.py +620 -586
  48. webscout/Provider/TextPollinationsAI.py +11 -10
  49. webscout/Provider/TogetherAI.py +12 -4
  50. webscout/Provider/TwoAI.py +96 -2
  51. webscout/Provider/TypliAI.py +33 -27
  52. webscout/Provider/UNFINISHED/VercelAIGateway.py +339 -0
  53. webscout/Provider/UNFINISHED/fetch_together_models.py +6 -11
  54. webscout/Provider/Venice.py +1 -0
  55. webscout/Provider/WiseCat.py +18 -20
  56. webscout/Provider/__init__.py +2 -96
  57. webscout/Provider/cerebras.py +83 -33
  58. webscout/Provider/copilot.py +42 -23
  59. webscout/Provider/scira_chat.py +4 -0
  60. webscout/Provider/toolbaz.py +6 -10
  61. webscout/Provider/typefully.py +1 -11
  62. webscout/__init__.py +3 -15
  63. webscout/auth/__init__.py +19 -4
  64. webscout/auth/api_key_manager.py +189 -189
  65. webscout/auth/auth_system.py +25 -40
  66. webscout/auth/config.py +105 -6
  67. webscout/auth/database.py +377 -22
  68. webscout/auth/models.py +185 -130
  69. webscout/auth/request_processing.py +175 -11
  70. webscout/auth/routes.py +99 -2
  71. webscout/auth/server.py +9 -2
  72. webscout/auth/simple_logger.py +236 -0
  73. webscout/conversation.py +22 -20
  74. webscout/sanitize.py +1078 -0
  75. webscout/scout/README.md +20 -23
  76. webscout/scout/core/crawler.py +125 -38
  77. webscout/scout/core/scout.py +26 -5
  78. webscout/version.py +1 -1
  79. webscout/webscout_search.py +13 -6
  80. webscout/webscout_search_async.py +10 -8
  81. webscout/yep_search.py +13 -5
  82. {webscout-8.3.4.dist-info → webscout-8.3.6.dist-info}/METADATA +10 -149
  83. {webscout-8.3.4.dist-info → webscout-8.3.6.dist-info}/RECORD +88 -87
  84. webscout/Provider/Glider.py +0 -225
  85. webscout/Provider/OPENAI/README_AUTOPROXY.md +0 -238
  86. webscout/Provider/OPENAI/c4ai.py +0 -394
  87. webscout/Provider/OPENAI/glider.py +0 -330
  88. webscout/Provider/OPENAI/typegpt.py +0 -368
  89. webscout/Provider/OPENAI/uncovrAI.py +0 -477
  90. webscout/Provider/TTS/sthir.py +0 -94
  91. webscout/Provider/WritingMate.py +0 -273
  92. webscout/Provider/typegpt.py +0 -284
  93. webscout/Provider/uncovr.py +0 -333
  94. /webscout/Provider/{samurai.py → UNFINISHED/samurai.py} +0 -0
  95. {webscout-8.3.4.dist-info → webscout-8.3.6.dist-info}/WHEEL +0 -0
  96. {webscout-8.3.4.dist-info → webscout-8.3.6.dist-info}/entry_points.txt +0 -0
  97. {webscout-8.3.4.dist-info → webscout-8.3.6.dist-info}/licenses/LICENSE.md +0 -0
  98. {webscout-8.3.4.dist-info → webscout-8.3.6.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) -> StreamingResponse:
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
- choice['delta']['content'] = clean_text(choice['delta']['content'])
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
- choice['message']['content'] = clean_text(choice['message']['content'])
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
- choice['delta']['content'] = clean_text(choice['delta']['content'])
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
- choice['message']['content'] = clean_text(choice['message']['content'])
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
- choice['delta']['content'] = clean_text(choice['delta']['content'])
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
- choice['message']['content'] = clean_text(choice['message']['content'])
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) -> Dict[str, Any]:
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
- return ChatCompletion(
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
- # Clean text content in the response to remove control characters
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
- choice['message']['content'] = clean_text(choice['message']['content'])
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,